Trying a different architecture to show fragments correctly depending on device size & orientation

This commit is contained in:
Sylvain Berfini 2023-08-07 09:55:32 +02:00
parent 6cccf09bc1
commit 5803fe18ed
18 changed files with 459 additions and 323 deletions

View file

@ -33,6 +33,7 @@ import org.linphone.R
import org.linphone.databinding.ContactFragmentBinding
import org.linphone.ui.contacts.viewmodel.ContactViewModel
import org.linphone.ui.viewmodel.SharedMainViewModel
import org.linphone.utils.Event
class ContactFragment : Fragment() {
private lateinit var binding: ContactFragmentBinding
@ -76,7 +77,7 @@ class ContactFragment : Fragment() {
viewModel.findContactByRefKey(refKey)
binding.setBackClickListener {
requireActivity().onBackPressedDispatcher.onBackPressed()
sharedViewModel.closeSlidingPaneEvent.value = Event(true)
}
sharedViewModel.isSlidingPaneSlideable.observe(viewLifecycleOwner) { slideable ->

View file

@ -19,53 +19,27 @@
*/
package org.linphone.ui.contacts
import android.content.res.Configuration
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.view.animation.Animation
import android.view.animation.AnimationUtils
import androidx.core.view.doOnPreDraw
import androidx.fragment.app.Fragment
import androidx.lifecycle.ViewModelProvider
import androidx.navigation.fragment.FragmentNavigatorExtras
import androidx.navigation.fragment.NavHostFragment
import androidx.navigation.findNavController
import androidx.navigation.fragment.findNavController
import androidx.navigation.navGraphViewModels
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.slidingpanelayout.widget.SlidingPaneLayout
import androidx.transition.AutoTransition
import org.linphone.R
import org.linphone.databinding.ContactsFragmentBinding
import org.linphone.ui.MainActivity
import org.linphone.ui.contacts.adapter.ContactsListAdapter
import org.linphone.ui.contacts.viewmodel.ContactsListViewModel
import org.linphone.ui.viewmodel.SharedMainViewModel
import org.linphone.utils.SlidingPaneBackPressedCallback
import org.linphone.utils.hideKeyboard
import org.linphone.utils.setKeyboardInsetListener
import org.linphone.utils.showKeyboard
class ContactsFragment : Fragment() {
private lateinit var binding: ContactsFragmentBinding
private lateinit var sharedViewModel: SharedMainViewModel
private val listViewModel: ContactsListViewModel by navGraphViewModels(
R.id.contactsFragment
)
private lateinit var adapter: ContactsListAdapter
override fun onCreateAnimation(transit: Int, enter: Boolean, nextAnim: Int): Animation? {
if (findNavController().currentDestination?.id == R.id.newContactFragment) {
// Holds fragment in place while new contact fragment slides over it
return AnimationUtils.loadAnimation(activity, R.anim.hold)
}
return super.onCreateAnimation(transit, enter, nextAnim)
}
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
@ -84,15 +58,7 @@ class ContactsFragment : Fragment() {
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
postponeEnterTransition()
binding.lifecycleOwner = viewLifecycleOwner
binding.viewModel = listViewModel
binding.root.setKeyboardInsetListener { keyboardVisible ->
val portraitOrientation = resources.configuration.orientation != Configuration.ORIENTATION_LANDSCAPE
listViewModel.bottomNavBarVisible.value = !portraitOrientation || !keyboardVisible
}
binding.root.doOnPreDraw {
val slidingPane = binding.slidingPaneLayout
@ -107,85 +73,39 @@ class ContactsFragment : Fragment() {
slidingPane.lockMode = SlidingPaneLayout.LOCK_MODE_LOCKED
}
adapter = ContactsListAdapter(viewLifecycleOwner)
binding.contactsView.contactsList.setHasFixedSize(true)
binding.contactsView.contactsList.adapter = adapter
adapter.contactLongClickedEvent.observe(viewLifecycleOwner) {
it.consume { model ->
val modalBottomSheet = ContactsListMenuDialogFragment(model.friend) {
adapter.resetSelection()
}
modalBottomSheet.show(parentFragmentManager, ContactsListMenuDialogFragment.TAG)
}
}
adapter.contactClickedEvent.observe(viewLifecycleOwner) {
it.consume { model ->
if (findNavController().currentDestination?.id == R.id.contactsFragment) {
val navHostFragment = childFragmentManager.findFragmentById(
R.id.contacts_nav_container
) as NavHostFragment
val action = ContactFragmentDirections.actionGlobalContactFragment(
model.id ?: ""
)
navHostFragment.navController.navigate(action)
if (!binding.slidingPaneLayout.isOpen) {
binding.slidingPaneLayout.openPane()
}
}
}
}
val layoutManager = LinearLayoutManager(requireContext())
binding.contactsView.contactsList.layoutManager = layoutManager
listViewModel.contactsList.observe(
sharedViewModel.closeSlidingPaneEvent.observe(
viewLifecycleOwner
) {
adapter.submitList(it)
(view.parent as? ViewGroup)?.doOnPreDraw {
startPostponedEnterTransition()
}
}
listViewModel.searchFilter.observe(
viewLifecycleOwner
) {
listViewModel.applyFilter()
}
listViewModel.focusSearchBarEvent.observe(viewLifecycleOwner) {
it.consume { show ->
if (show) {
// To automatically open keyboard
binding.topBar.search.showKeyboard(requireActivity().window)
} else {
binding.topBar.search.hideKeyboard()
it.consume { close ->
if (close) {
binding.slidingPaneLayout.closePane()
}
}
}
binding.setOnNewContactClicked {
if (findNavController().currentDestination?.id == R.id.contactsFragment) {
findNavController().navigate(R.id.action_contactsFragment_to_newContactFragment)
}
}
binding.setOnConversationsClicked {
if (findNavController().currentDestination?.id == R.id.contactsFragment) {
val extras = FragmentNavigatorExtras(
binding.bottomNavBar.root to "bottom_nav_bar"
sharedViewModel.showContactEvent.observe(
viewLifecycleOwner
) {
it.consume { refKey ->
val navController = binding.contactsRightNavContainer.findNavController()
val action = ContactFragmentDirections.actionGlobalContactFragment(
refKey
)
val action = ContactsFragmentDirections.actionContactsFragmentToConversationsFragment()
findNavController().navigate(action, extras)
navController.navigate(action)
if (!binding.slidingPaneLayout.isOpen) {
binding.slidingPaneLayout.openPane()
}
}
}
binding.setOnAvatarClickListener {
(requireActivity() as MainActivity).toggleDrawerMenu()
sharedViewModel.navigateToConversationsEvent.observe(viewLifecycleOwner) {
it.consume {
if (findNavController().currentDestination?.id == R.id.contactsFragment) {
val action = ContactsFragmentDirections.actionContactsFragmentToConversationsFragment()
findNavController().navigate(action)
}
}
}
}
}

View file

@ -0,0 +1,153 @@
/*
* 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.contacts
import android.content.res.Configuration
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.view.animation.Animation
import android.view.animation.AnimationUtils
import androidx.core.view.doOnPreDraw
import androidx.fragment.app.Fragment
import androidx.lifecycle.ViewModelProvider
import androidx.navigation.fragment.findNavController
import androidx.navigation.navGraphViewModels
import androidx.recyclerview.widget.LinearLayoutManager
import org.linphone.R
import org.linphone.databinding.ContactsListFragmentBinding
import org.linphone.ui.MainActivity
import org.linphone.ui.contacts.adapter.ContactsListAdapter
import org.linphone.ui.contacts.viewmodel.ContactsListViewModel
import org.linphone.ui.viewmodel.SharedMainViewModel
import org.linphone.utils.Event
import org.linphone.utils.hideKeyboard
import org.linphone.utils.setKeyboardInsetListener
import org.linphone.utils.showKeyboard
class ContactsListFragment : Fragment() {
private lateinit var binding: ContactsListFragmentBinding
private lateinit var sharedViewModel: SharedMainViewModel
private val listViewModel: ContactsListViewModel by navGraphViewModels(
R.id.contactsListFragment
)
private lateinit var adapter: ContactsListAdapter
override fun onCreateAnimation(transit: Int, enter: Boolean, nextAnim: Int): Animation? {
if (findNavController().currentDestination?.id == R.id.newContactFragment) {
// Holds fragment in place while new contact fragment slides over it
return AnimationUtils.loadAnimation(activity, R.anim.hold)
}
return super.onCreateAnimation(transit, enter, nextAnim)
}
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View {
binding = ContactsListFragmentBinding.inflate(layoutInflater)
sharedViewModel = requireActivity().run {
ViewModelProvider(this)[SharedMainViewModel::class.java]
}
return binding.root
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
postponeEnterTransition()
binding.lifecycleOwner = viewLifecycleOwner
binding.viewModel = listViewModel
binding.root.setKeyboardInsetListener { keyboardVisible ->
val portraitOrientation = resources.configuration.orientation != Configuration.ORIENTATION_LANDSCAPE
listViewModel.bottomNavBarVisible.value = !portraitOrientation || !keyboardVisible
}
adapter = ContactsListAdapter(viewLifecycleOwner)
binding.contactsList.setHasFixedSize(true)
binding.contactsList.adapter = adapter
adapter.contactLongClickedEvent.observe(viewLifecycleOwner) {
it.consume { model ->
val modalBottomSheet = ContactsListMenuDialogFragment(model.friend) {
adapter.resetSelection()
}
modalBottomSheet.show(parentFragmentManager, ContactsListMenuDialogFragment.TAG)
}
}
adapter.contactClickedEvent.observe(viewLifecycleOwner) {
it.consume { model ->
sharedViewModel.showContactEvent.value = Event(model.id ?: "")
}
}
val layoutManager = LinearLayoutManager(requireContext())
binding.contactsList.layoutManager = layoutManager
listViewModel.contactsList.observe(
viewLifecycleOwner
) {
adapter.submitList(it)
(view.parent as? ViewGroup)?.doOnPreDraw {
startPostponedEnterTransition()
}
}
listViewModel.focusSearchBarEvent.observe(viewLifecycleOwner) {
it.consume { show ->
if (show) {
// To automatically open keyboard
binding.topBar.search.showKeyboard(requireActivity().window)
} else {
binding.topBar.search.hideKeyboard()
}
}
}
listViewModel.searchFilter.observe(viewLifecycleOwner) { filter ->
listViewModel.applyFilter(filter)
}
binding.setOnNewContactClicked {
if (findNavController().currentDestination?.id == R.id.contactsListFragment) {
findNavController().navigate(R.id.action_contactsListFragment_to_newContactFragment)
}
}
binding.setOnConversationsClicked {
sharedViewModel.navigateToConversationsEvent.value = Event(true)
}
binding.setOnAvatarClickListener {
(requireActivity() as MainActivity).toggleDrawerMenu()
}
}
}

View file

@ -25,6 +25,7 @@ import android.view.View
import android.view.ViewGroup
import android.view.animation.Animation
import androidx.fragment.app.Fragment
import androidx.navigation.fragment.findNavController
import org.linphone.databinding.NewContactFragmentBinding
class NewContactFragment : Fragment() {
@ -55,7 +56,7 @@ class NewContactFragment : Fragment() {
// postponeEnterTransition()
binding.setCancelClickListener {
requireActivity().onBackPressedDispatcher.onBackPressed()
findNavController().popBackStack()
}
/*(view.parent as? ViewGroup)?.doOnPreDraw {

View file

@ -25,40 +25,57 @@ import org.linphone.LinphoneApplication.Companion.coreContext
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.contacts.model.ContactModel
import org.linphone.ui.viewmodel.TopBarViewModel
class ContactsListViewModel : TopBarViewModel() {
val bottomNavBarVisible = MutableLiveData<Boolean>()
val contactsList = MutableLiveData<ArrayList<ContactModel>>()
private var currentFilter = ""
private var previousFilter = "NotSet"
private lateinit var magicSearch: MagicSearch
private val magicSearchListener = object : MagicSearchListenerStub() {
override fun onSearchResultsReceived(magicSearch: MagicSearch) {
// Core thread
Log.i("[Contacts] Magic search contacts available")
processMagicSearchResults(magicSearch.lastSearch)
}
}
private val contactsListener = object : ContactsListener {
override fun onContactsLoaded() {
// Core thread
applyFilter()
applyFilter(currentFilter)
}
}
init {
title.value = "Contacts"
bottomNavBarVisible.value = true
coreContext.postOnCoreThread {
coreContext.postOnCoreThread { core ->
coreContext.contactsManager.addListener(contactsListener)
magicSearch = core.createMagicSearch()
magicSearch.limitedSearch = false
magicSearch.addListener(magicSearchListener)
}
applyFilter()
applyFilter(currentFilter)
}
override fun onCleared() {
coreContext.postOnCoreThread {
magicSearch.removeListener(magicSearchListener)
coreContext.contactsManager.removeListener(contactsListener)
}
super.onCleared()
}
override fun processMagicSearchResults(results: Array<SearchResult>) {
fun processMagicSearchResults(results: Array<SearchResult>) {
// Core thread
Log.i("[Contacts List] Processing ${results.size} results")
contactsList.value.orEmpty().forEach(ContactModel::destroy)
@ -85,9 +102,11 @@ class ContactsListViewModel : TopBarViewModel() {
Log.i("[Contacts] Processed ${results.size} results")
}
fun applyFilter() {
fun applyFilter(filter: String) {
// UI thread
coreContext.postOnCoreThread {
applyFilter(
filter,
"",
MagicSearch.Source.Friends.toInt(),
MagicSearch.Aggregation.Friend
@ -95,6 +114,34 @@ class ContactsListViewModel : TopBarViewModel() {
}
}
private fun applyFilter(
filter: String,
domain: String,
sources: Int,
aggregation: MagicSearch.Aggregation
) {
// Core thread
if (previousFilter.isNotEmpty() && (
previousFilter.length > filter.length ||
(previousFilter.length == filter.length && previousFilter != filter)
)
) {
magicSearch.resetSearchCache()
}
currentFilter = filter
previousFilter = filter
Log.i(
"[Contacts] Asking Magic search for contacts matching filter [$filter], domain [$domain] and in sources [$sources]"
)
magicSearch.getContactsListAsync(
filter,
domain,
sources,
aggregation
)
}
private fun createFriendFromSearchResult(searchResult: SearchResult): Friend {
// Core thread
val searchResultFriend = searchResult.friend

View file

@ -166,11 +166,8 @@ class ConversationsFragment : Fragment() {
binding.setOnContactsClicked {
if (findNavController().currentDestination?.id == R.id.conversationsFragment) {
val extras = FragmentNavigatorExtras(
binding.bottomNavBar.root to "bottom_nav_bar"
)
val action = ConversationsFragmentDirections.actionConversationsFragmentToContactsFragment()
findNavController().navigate(action, extras)
findNavController().navigate(action)
}
}
}

View file

@ -27,7 +27,6 @@ import org.linphone.core.ChatMessage
import org.linphone.core.ChatRoom
import org.linphone.core.Core
import org.linphone.core.CoreListenerStub
import org.linphone.core.SearchResult
import org.linphone.core.tools.Log
import org.linphone.ui.conversations.data.ChatRoomData
import org.linphone.ui.viewmodel.TopBarViewModel
@ -114,10 +113,6 @@ class ConversationsListViewModel : TopBarViewModel() {
super.onCleared()
}
override fun processMagicSearchResults(results: Array<SearchResult>) {
TODO("Not yet implemented")
}
private fun addChatRoomToList(chatRoom: ChatRoom) {
val index = findChatRoomIndex(chatRoom)
if (index != -1) {

View file

@ -21,7 +21,18 @@ package org.linphone.ui.viewmodel
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.ViewModel
import org.linphone.utils.Event
class SharedMainViewModel : ViewModel() {
/* Sliding Pane & navigation related */
val isSlidingPaneSlideable = MutableLiveData<Boolean>()
val closeSlidingPaneEvent = MutableLiveData<Event<Boolean>>()
val navigateToConversationsEvent = MutableLiveData<Event<Boolean>>()
/* Contacts related */
val showContactEvent = MutableLiveData<Event<String>>()
}

View file

@ -21,11 +21,6 @@ package org.linphone.ui.viewmodel
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.ViewModel
import org.linphone.LinphoneApplication.Companion.coreContext
import org.linphone.core.MagicSearch
import org.linphone.core.MagicSearchListenerStub
import org.linphone.core.SearchResult
import org.linphone.core.tools.Log
import org.linphone.utils.Event
abstract class TopBarViewModel : ViewModel() {
@ -39,31 +34,13 @@ abstract class TopBarViewModel : ViewModel() {
MutableLiveData<Event<Boolean>>()
}
private var previousFilter = "NotSet"
private lateinit var magicSearch: MagicSearch
private val magicSearchListener = object : MagicSearchListenerStub() {
override fun onSearchResultsReceived(magicSearch: MagicSearch) {
// Core thread
Log.i("[Contacts] Magic search contacts available")
processMagicSearchResults(magicSearch.lastSearch)
}
}
val bottomNavBarVisible = MutableLiveData<Boolean>()
init {
searchBarVisible.value = false
coreContext.postOnCoreThread { core ->
magicSearch = core.createMagicSearch()
magicSearch.limitedSearch = false
magicSearch.addListener(magicSearchListener)
}
}
override fun onCleared() {
coreContext.postOnCoreThread { core ->
magicSearch.removeListener(magicSearchListener)
}
super.onCleared()
}
@ -83,29 +60,4 @@ abstract class TopBarViewModel : ViewModel() {
// UI thread
searchFilter.value = ""
}
fun applyFilter(domain: String, sources: Int, aggregation: MagicSearch.Aggregation) {
// Core thread
val filterValue = searchFilter.value.orEmpty()
if (previousFilter.isNotEmpty() && (
previousFilter.length > filterValue.length ||
(previousFilter.length == filterValue.length && previousFilter != filterValue)
)
) {
magicSearch.resetSearchCache()
}
previousFilter = filterValue
Log.i(
"[Contacts] Asking Magic search for contacts matching filter [$filterValue], domain [$domain] and in sources [$sources]"
)
magicSearch.getContactsListAsync(
filterValue,
domain,
sources,
aggregation
)
}
abstract fun processMagicSearchResults(results: Array<SearchResult>)
}

View file

@ -30,7 +30,6 @@
android:id="@+id/bottom_nav_bar"
android:layout_width="75dp"
android:layout_height="match_parent"
android:transitionName="bottom_nav_bar"
android:background="@color/white">
<androidx.appcompat.widget.AppCompatTextView

View file

@ -1,99 +1,40 @@
<?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"
xmlns:bind="http://schemas.android.com/tools">
xmlns:app="http://schemas.android.com/apk/res-auto">
<data>
<import type="android.view.View" />
<variable
name="onAvatarClickListener"
type="View.OnClickListener" />
<variable
name="onNewContactClicked"
type="View.OnClickListener" />
<variable
name="onConversationsClicked"
type="View.OnClickListener" />
<variable
name="viewModel"
type="org.linphone.ui.contacts.viewmodel.ContactsListViewModel" />
</data>
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="match_parent">
<include
bind:onConversationsClicked="@{onConversationsClicked}"
bind:contactsSelected="@{true}"
android:visibility="@{viewModel.bottomNavBarVisible ? View.VISIBLE : View.GONE}"
android:id="@+id/bottom_nav_bar"
layout="@layout/bottom_nav_bar"
android:layout_width="75dp"
android:layout_height="0dp"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintBottom_toBottomOf="parent"/>
<include
android:id="@+id/top_bar"
bind:viewModel="@{viewModel}"
bind:onAvatarClickListener="@{onAvatarClickListener}"
layout="@layout/top_search_bar"
android:layout_height="wrap_content"
android:layout_width="0dp"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintStart_toEndOf="@id/bottom_nav_bar"
app:layout_constraintEnd_toEndOf="parent"/>
<androidx.slidingpanelayout.widget.SlidingPaneLayout
android:id="@+id/sliding_pane_layout"
android:layout_width="0dp"
android:layout_height="0dp"
app:layout_constraintTop_toBottomOf="@id/top_bar"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintStart_toEndOf="@id/bottom_nav_bar"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent">
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="280dp"
<androidx.fragment.app.FragmentContainerView
android:id="@+id/contacts_left_nav_container"
android:name="androidx.navigation.fragment.NavHostFragment"
android:layout_width="355dp"
android:layout_height="match_parent"
android:background="@color/primary_color">
<include
android:id="@+id/contacts_view"
layout="@layout/contacts_list_fragment"
bind:viewModel="@{viewModel}"
android:layout_width="0dp"
android:layout_height="match_parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"/>
<com.google.android.material.floatingactionbutton.FloatingActionButton
android:id="@+id/new_contact"
android:onClick="@{onNewContactClicked}"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="end|bottom"
android:layout_margin="16dp"
android:src="@drawable/add"
app:tint="@color/gray_8"
app:backgroundTint="@color/white"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintBottom_toBottomOf="parent"/>
</androidx.constraintlayout.widget.ConstraintLayout>
app:defaultNavHost="false"
app:navGraph="@navigation/contact_left_nav_graph"/>
<androidx.fragment.app.FragmentContainerView
android:id="@+id/contacts_nav_container"
android:id="@+id/contacts_right_nav_container"
android:name="androidx.navigation.fragment.NavHostFragment"
android:layout_width="0dp"
android:layout_height="match_parent"
android:layout_weight="1"
app:defaultNavHost="false"
app:navGraph="@navigation/contact_nav_graph"
app:navGraph="@navigation/contact_right_nav_graph"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintStart_toEndOf="@id/contactsList"

View file

@ -0,0 +1,120 @@
<?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"
xmlns:bind="http://schemas.android.com/tools">
<data>
<import type="android.view.View" />
<variable
name="onNewContactClicked"
type="View.OnClickListener" />
<variable
name="onAvatarClickListener"
type="View.OnClickListener" />
<variable
name="onConversationsClicked"
type="View.OnClickListener" />
<variable
name="viewModel"
type="org.linphone.ui.contacts.viewmodel.ContactsListViewModel" />
</data>
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="@color/primary_color">
<androidx.constraintlayout.widget.Group
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:constraint_referenced_ids="no_contacts_image, no_contacts_label"
android:visibility="@{viewModel.contactsList.empty ? View.VISIBLE : View.GONE}" />
<include
bind:onConversationsClicked="@{onConversationsClicked}"
bind:contactsSelected="@{true}"
android:id="@+id/bottom_nav_bar"
android:visibility="@{viewModel.bottomNavBarVisible ? View.VISIBLE : View.GONE}"
layout="@layout/bottom_nav_bar"
android:layout_width="75dp"
android:layout_height="0dp"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintBottom_toBottomOf="parent"/>
<include
android:id="@+id/top_bar"
bind:viewModel="@{viewModel}"
bind:onAvatarClickListener="@{onAvatarClickListener}"
layout="@layout/top_search_bar"
android:layout_height="wrap_content"
android:layout_width="0dp"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintStart_toEndOf="@id/bottom_nav_bar"
app:layout_constraintEnd_toEndOf="parent" />
<ImageView
android:id="@+id/background"
android:layout_width="0dp"
android:layout_height="0dp"
android:layout_marginTop="7dp"
android:src="@drawable/shape_white_background"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toEndOf="@id/bottom_nav_bar"
app:layout_constraintTop_toBottomOf="@id/top_bar"/>
<ImageView
android:id="@+id/no_contacts_image"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:src="@drawable/illu"
android:layout_margin="10dp"
app:layout_constraintHeight_max="200dp"
app:layout_constraintBottom_toTopOf="@id/no_contacts_label"
app:layout_constraintDimensionRatio="1:1"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toEndOf="@id/bottom_nav_bar"
app:layout_constraintTop_toBottomOf="@id/background" />
<TextView
android:id="@+id/no_contacts_label"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="No contacts for the moment..."
android:textColor="@color/gray_9"
android:textSize="16sp"
android:textStyle="bold"
app:layout_constraintBottom_toTopOf="@id/background"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toEndOf="@id/bottom_nav_bar"
app:layout_constraintTop_toBottomOf="@id/no_contacts_image" />
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/contactsList"
android:layout_width="0dp"
android:layout_height="0dp"
android:layout_marginTop="20dp"
android:layout_marginStart="10dp"
android:layout_marginEnd="10dp"
app:layout_constraintStart_toEndOf="@id/bottom_nav_bar"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="@id/background"
app:layout_constraintBottom_toBottomOf="parent" />
<com.google.android.material.floatingactionbutton.FloatingActionButton
android:id="@+id/new_contact"
android:onClick="@{onNewContactClicked}"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="end|bottom"
android:layout_margin="16dp"
android:src="@drawable/add"
app:tint="@color/gray_8"
app:backgroundTint="@color/white"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintBottom_toBottomOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>
</layout>

View file

@ -27,7 +27,6 @@
</data>
<androidx.constraintlayout.widget.ConstraintLayout
android:transitionName="bottom_nav_bar"
android:id="@+id/bottom_nav_bar"
android:layout_width="match_parent"
android:layout_height="wrap_content"

View file

@ -1,22 +1,9 @@
<?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"
xmlns:bind="http://schemas.android.com/tools">
xmlns:app="http://schemas.android.com/apk/res-auto">
<data>
<import type="android.view.View" />
<variable
name="onAvatarClickListener"
type="View.OnClickListener" />
<variable
name="onNewContactClicked"
type="View.OnClickListener" />
<variable
name="onConversationsClicked"
type="View.OnClickListener" />
<variable
name="viewModel"
type="org.linphone.ui.contacts.viewmodel.ContactsListViewModel" />
</data>
<androidx.slidingpanelayout.widget.SlidingPaneLayout
@ -29,58 +16,28 @@
android:layout_height="match_parent"
android:background="@color/primary_color">
<include
android:id="@+id/top_bar"
bind:viewModel="@{viewModel}"
bind:onAvatarClickListener="@{onAvatarClickListener}"
layout="@layout/top_search_bar" />
<include
android:id="@+id/contacts_view"
layout="@layout/contacts_list_fragment"
bind:viewModel="@{viewModel}"
<androidx.fragment.app.FragmentContainerView
android:id="@+id/contacts_left_nav_container"
android:name="androidx.navigation.fragment.NavHostFragment"
android:layout_width="0dp"
android:layout_height="0dp"
app:defaultNavHost="false"
app:navGraph="@navigation/contact_left_nav_graph"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/top_bar"
app:layout_constraintBottom_toTopOf="@id/bottom_nav_bar"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"/>
<include
bind:onConversationsClicked="@{onConversationsClicked}"
bind:contactsSelected="@{true}"
android:visibility="@{viewModel.bottomNavBarVisible ? View.VISIBLE : View.GONE}"
android:id="@+id/bottom_nav_bar"
layout="@layout/bottom_nav_bar"
android:layout_width="0dp"
android:layout_height="wrap_content"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintBottom_toBottomOf="parent"/>
<com.google.android.material.floatingactionbutton.FloatingActionButton
android:id="@+id/new_contact"
android:onClick="@{onNewContactClicked}"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="end|bottom"
android:layout_margin="16dp"
android:src="@drawable/add"
app:tint="@color/gray_8"
app:backgroundTint="@color/white"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintBottom_toTopOf="@id/bottom_nav_bar" />
</androidx.constraintlayout.widget.ConstraintLayout>
<androidx.fragment.app.FragmentContainerView
android:id="@+id/contacts_nav_container"
android:id="@+id/contacts_right_nav_container"
android:name="androidx.navigation.fragment.NavHostFragment"
android:layout_width="300dp"
android:layout_height="match_parent"
android:layout_weight="1"
app:defaultNavHost="false"
app:navGraph="@navigation/contact_nav_graph"/>
app:navGraph="@navigation/contact_right_nav_graph"/>
</androidx.slidingpanelayout.widget.SlidingPaneLayout>

View file

@ -5,6 +5,15 @@
<data>
<import type="android.view.View" />
<variable
name="onNewContactClicked"
type="View.OnClickListener" />
<variable
name="onAvatarClickListener"
type="View.OnClickListener" />
<variable
name="onConversationsClicked"
type="View.OnClickListener" />
<variable
name="viewModel"
type="org.linphone.ui.contacts.viewmodel.ContactsListViewModel" />
@ -21,6 +30,12 @@
app:constraint_referenced_ids="no_contacts_image, no_contacts_label"
android:visibility="@{viewModel.contactsList.empty ? View.VISIBLE : View.GONE}" />
<include
android:id="@+id/top_bar"
bind:viewModel="@{viewModel}"
bind:onAvatarClickListener="@{onAvatarClickListener}"
layout="@layout/top_search_bar" />
<ImageView
android:id="@+id/background"
android:layout_width="0dp"
@ -30,7 +45,7 @@
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
app:layout_constraintTop_toBottomOf="@id/top_bar" />
<ImageView
android:id="@+id/no_contacts_image"
@ -70,6 +85,31 @@
app:layout_constraintTop_toTopOf="@id/background"
app:layout_constraintBottom_toBottomOf="parent" />
<include
bind:onConversationsClicked="@{onConversationsClicked}"
bind:contactsSelected="@{true}"
android:id="@+id/bottom_nav_bar"
android:visibility="@{viewModel.bottomNavBarVisible ? View.VISIBLE : View.GONE}"
layout="@layout/bottom_nav_bar"
android:layout_width="0dp"
android:layout_height="wrap_content"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintBottom_toBottomOf="parent"/>
<com.google.android.material.floatingactionbutton.FloatingActionButton
android:id="@+id/new_contact"
android:onClick="@{onNewContactClicked}"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="end|bottom"
android:layout_margin="16dp"
android:src="@drawable/add"
app:tint="@color/gray_8"
app:backgroundTint="@color/white"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintBottom_toTopOf="@id/bottom_nav_bar" />
</androidx.constraintlayout.widget.ConstraintLayout>
</layout>

View file

@ -0,0 +1,27 @@
<?xml version="1.0" encoding="utf-8"?>
<navigation 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:id="@+id/contact_left_nav_graph"
app:startDestination="@id/contactsListFragment">
<fragment
android:id="@+id/contactsListFragment"
android:name="org.linphone.ui.contacts.ContactsListFragment"
android:label="ContactsListFragment"
tools:layout="@layout/contacts_list_fragment">
<action
android:id="@+id/action_contactsListFragment_to_newContactFragment"
app:destination="@id/newContactFragment"
app:enterAnim="@anim/slide_in"
app:launchSingleTop="true"
app:popExitAnim="@anim/slide_out" />
</fragment>
<fragment
android:id="@+id/newContactFragment"
android:name="org.linphone.ui.contacts.NewContactFragment"
android:label="NewContactFragment"
tools:layout="@layout/new_contact_fragment"/>
</navigation>

View file

@ -2,7 +2,7 @@
<navigation 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:id="@+id/contact_nav_graph"
android:id="@+id/contact_right_nav_graph"
app:startDestination="@id/emptyFragment">
<fragment

View file

@ -60,33 +60,9 @@
android:name="org.linphone.ui.contacts.ContactsFragment"
android:label="ContactsFragment"
tools:layout="@layout/contacts_fragment">
<action
android:id="@+id/action_contactsFragment_to_newContactFragment"
app:destination="@id/newContactFragment"
app:enterAnim="@anim/slide_in"
app:popExitAnim="@anim/slide_out" />
<action
android:id="@+id/action_contactsFragment_to_conversationsFragment"
app:destination="@id/conversationsFragment"
app:launchSingleTop="true"
app:popUpTo="@id/conversationsFragment"
app:popUpToInclusive="true" />
app:destination="@id/conversationsFragment" />
</fragment>
<fragment
android:id="@+id/contactFragment"
android:name="org.linphone.ui.contacts.ContactFragment"
android:label="ContactFragment"
tools:layout="@layout/contact_fragment" >
<argument
android:name="contactRefKey"
app:argType="string" />
</fragment>
<fragment
android:id="@+id/newContactFragment"
android:name="org.linphone.ui.contacts.NewContactFragment"
android:label="NewContactFragment"
tools:layout="@layout/new_contact_fragment" />
</navigation>