mirror of
https://gitlab.linphone.org/BC/public/linphone-android.git
synced 2026-01-17 11:28:06 +00:00
Merged contacts & suggestions in the same list
This commit is contained in:
parent
82ae513d17
commit
9c9391c95b
19 changed files with 360 additions and 125 deletions
|
|
@ -0,0 +1,146 @@
|
|||
package org.linphone.ui.main.calls.adapter
|
||||
|
||||
import android.content.Context
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
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.CallSuggestionListCellBinding
|
||||
import org.linphone.databinding.ContactListCellBinding
|
||||
import org.linphone.ui.main.calls.model.ContactOrSuggestionModel
|
||||
import org.linphone.utils.Event
|
||||
import org.linphone.utils.HeaderAdapter
|
||||
|
||||
class ContactsAndSuggestionsListAdapter(
|
||||
private val viewLifecycleOwner: LifecycleOwner
|
||||
) : ListAdapter<ContactOrSuggestionModel, RecyclerView.ViewHolder>(
|
||||
ContactOrSuggestionDiffCallback()
|
||||
),
|
||||
HeaderAdapter {
|
||||
companion object {
|
||||
private const val CONTACT_TYPE = 0
|
||||
private const val SUGGESTION_TYPE = 1
|
||||
}
|
||||
|
||||
var selectedAdapterPosition = -1
|
||||
|
||||
val contactClickedEvent: MutableLiveData<Event<ContactOrSuggestionModel>> by lazy {
|
||||
MutableLiveData<Event<ContactOrSuggestionModel>>()
|
||||
}
|
||||
|
||||
override fun displayHeaderForPosition(position: Int): Boolean {
|
||||
val model = getItem(position)
|
||||
if (model.friend == null) {
|
||||
if (position == 0) {
|
||||
return true
|
||||
}
|
||||
val previousModel = getItem(position - 1)
|
||||
return previousModel.friend != null
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
override fun getHeaderViewForPosition(context: Context, position: Int): View {
|
||||
return LayoutInflater.from(context).inflate(R.layout.call_suggestion_list_decoration, null)
|
||||
}
|
||||
|
||||
override fun getItemViewType(position: Int): Int {
|
||||
val model = getItem(position)
|
||||
return if (model.friend == null) SUGGESTION_TYPE else CONTACT_TYPE
|
||||
}
|
||||
|
||||
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder {
|
||||
return when (viewType) {
|
||||
CONTACT_TYPE -> {
|
||||
val binding: ContactListCellBinding = DataBindingUtil.inflate(
|
||||
LayoutInflater.from(parent.context),
|
||||
R.layout.contact_list_cell,
|
||||
parent,
|
||||
false
|
||||
)
|
||||
ContactViewHolder(binding)
|
||||
}
|
||||
else -> {
|
||||
val binding: CallSuggestionListCellBinding = DataBindingUtil.inflate(
|
||||
LayoutInflater.from(parent.context),
|
||||
R.layout.call_suggestion_list_cell,
|
||||
parent,
|
||||
false
|
||||
)
|
||||
SuggestionViewHolder(binding)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) {
|
||||
when (getItemViewType(position)) {
|
||||
CONTACT_TYPE -> (holder as ContactViewHolder).bind(getItem(position))
|
||||
else -> (holder as SuggestionViewHolder).bind(getItem(position))
|
||||
}
|
||||
}
|
||||
|
||||
inner class ContactViewHolder(
|
||||
val binding: ContactListCellBinding
|
||||
) : RecyclerView.ViewHolder(binding.root) {
|
||||
@UiThread
|
||||
fun bind(contactOrSuggestionModel: ContactOrSuggestionModel) {
|
||||
with(binding) {
|
||||
model = contactOrSuggestionModel.contactAvatarModel
|
||||
|
||||
lifecycleOwner = viewLifecycleOwner
|
||||
|
||||
binding.root.isSelected = bindingAdapterPosition == selectedAdapterPosition
|
||||
|
||||
binding.setOnClickListener {
|
||||
contactClickedEvent.value = Event(contactOrSuggestionModel)
|
||||
}
|
||||
|
||||
executePendingBindings()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
inner class SuggestionViewHolder(
|
||||
val binding: CallSuggestionListCellBinding
|
||||
) : RecyclerView.ViewHolder(binding.root) {
|
||||
@UiThread
|
||||
fun bind(contactOrSuggestionModel: ContactOrSuggestionModel) {
|
||||
with(binding) {
|
||||
model = contactOrSuggestionModel
|
||||
|
||||
lifecycleOwner = viewLifecycleOwner
|
||||
|
||||
binding.root.isSelected = bindingAdapterPosition == selectedAdapterPosition
|
||||
|
||||
binding.setOnClickListener {
|
||||
contactClickedEvent.value = Event(contactOrSuggestionModel)
|
||||
}
|
||||
|
||||
executePendingBindings()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private class ContactOrSuggestionDiffCallback : DiffUtil.ItemCallback<ContactOrSuggestionModel>() {
|
||||
override fun areItemsTheSame(
|
||||
oldItem: ContactOrSuggestionModel,
|
||||
newItem: ContactOrSuggestionModel
|
||||
): Boolean {
|
||||
return oldItem.id == newItem.id
|
||||
}
|
||||
|
||||
override fun areContentsTheSame(
|
||||
oldItem: ContactOrSuggestionModel,
|
||||
newItem: ContactOrSuggestionModel
|
||||
): Boolean {
|
||||
return false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -34,16 +34,16 @@ import org.linphone.R
|
|||
import org.linphone.contacts.getListOfSipAddressesAndPhoneNumbers
|
||||
import org.linphone.core.tools.Log
|
||||
import org.linphone.databinding.CallStartFragmentBinding
|
||||
import org.linphone.ui.main.calls.adapter.ContactsAndSuggestionsListAdapter
|
||||
import org.linphone.ui.main.calls.model.ContactOrSuggestionModel
|
||||
import org.linphone.ui.main.calls.viewmodel.StartCallViewModel
|
||||
import org.linphone.ui.main.contacts.adapter.ContactsListAdapter
|
||||
import org.linphone.ui.main.contacts.model.ContactAvatarModel
|
||||
import org.linphone.ui.main.contacts.model.ContactNumberOrAddressClickListener
|
||||
import org.linphone.ui.main.contacts.model.ContactNumberOrAddressModel
|
||||
import org.linphone.ui.main.contacts.model.NumberOrAddressPickerDialogModel
|
||||
import org.linphone.ui.main.contacts.viewmodel.ContactsListViewModel
|
||||
import org.linphone.ui.main.fragment.GenericFragment
|
||||
import org.linphone.ui.main.model.isInSecureMode
|
||||
import org.linphone.utils.DialogUtils
|
||||
import org.linphone.utils.RecyclerViewHeaderDecoration
|
||||
import org.linphone.utils.hideKeyboard
|
||||
import org.linphone.utils.setKeyboardInsetListener
|
||||
import org.linphone.utils.showKeyboard
|
||||
|
|
@ -60,11 +60,7 @@ class StartCallFragment : GenericFragment() {
|
|||
R.id.main_nav_graph
|
||||
)
|
||||
|
||||
private val contactsListViewModel: ContactsListViewModel by navGraphViewModels(
|
||||
R.id.main_nav_graph
|
||||
)
|
||||
|
||||
private lateinit var contactsAdapter: ContactsListAdapter
|
||||
private lateinit var adapter: ContactsAndSuggestionsListAdapter
|
||||
|
||||
private val listener = object : ContactNumberOrAddressClickListener {
|
||||
@UiThread
|
||||
|
|
@ -108,42 +104,37 @@ class StartCallFragment : GenericFragment() {
|
|||
viewModel.hideNumpad()
|
||||
}
|
||||
|
||||
contactsAdapter = ContactsListAdapter(viewLifecycleOwner, disableLongClick = true)
|
||||
binding.contactsList.setHasFixedSize(true)
|
||||
binding.contactsList.adapter = contactsAdapter
|
||||
adapter = ContactsAndSuggestionsListAdapter(viewLifecycleOwner)
|
||||
binding.contactsAndSuggestionsList.setHasFixedSize(true)
|
||||
binding.contactsAndSuggestionsList.adapter = adapter
|
||||
|
||||
contactsAdapter.contactClickedEvent.observe(viewLifecycleOwner) {
|
||||
val headerItemDecoration = RecyclerViewHeaderDecoration(requireContext(), adapter)
|
||||
binding.contactsAndSuggestionsList.addItemDecoration(headerItemDecoration)
|
||||
|
||||
adapter.contactClickedEvent.observe(viewLifecycleOwner) {
|
||||
it.consume { model ->
|
||||
startCall(model)
|
||||
}
|
||||
}
|
||||
|
||||
viewModel.onSuggestionClickedEvent.observe(viewLifecycleOwner) {
|
||||
it.consume { address ->
|
||||
coreContext.postOnCoreThread {
|
||||
coreContext.startCall(address)
|
||||
}
|
||||
}
|
||||
}
|
||||
binding.contactsAndSuggestionsList.layoutManager = LinearLayoutManager(requireContext())
|
||||
|
||||
binding.contactsList.layoutManager = LinearLayoutManager(requireContext())
|
||||
|
||||
contactsListViewModel.contactsList.observe(
|
||||
viewModel.contactsAndSuggestionsList.observe(
|
||||
viewLifecycleOwner
|
||||
) {
|
||||
Log.i("$TAG Contacts list is ready with [${it.size}] items")
|
||||
contactsAdapter.submitList(it)
|
||||
viewModel.emptyContactsList.value = it.isEmpty()
|
||||
Log.i("$TAG Contacts & suggestions list is ready with [${it.size}] items")
|
||||
val count = adapter.itemCount
|
||||
adapter.submitList(it)
|
||||
|
||||
Log.i("$TAG Suggestions list is also ready, start postponed enter transition")
|
||||
(view.parent as? ViewGroup)?.doOnPreDraw {
|
||||
startPostponedEnterTransition()
|
||||
if (count == 0 && it.isNotEmpty()) {
|
||||
(view.parent as? ViewGroup)?.doOnPreDraw {
|
||||
startPostponedEnterTransition()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
viewModel.searchFilter.observe(viewLifecycleOwner) { filter ->
|
||||
val trimmed = filter.trim()
|
||||
contactsListViewModel.applyFilter(trimmed)
|
||||
viewModel.applyFilter(trimmed)
|
||||
}
|
||||
|
||||
|
|
@ -205,9 +196,14 @@ class StartCallFragment : GenericFragment() {
|
|||
numberOrAddressPickerDialog = null
|
||||
}
|
||||
|
||||
private fun startCall(model: ContactAvatarModel) {
|
||||
private fun startCall(model: ContactOrSuggestionModel) {
|
||||
coreContext.postOnCoreThread { core ->
|
||||
val friend = model.friend
|
||||
if (friend == null) {
|
||||
coreContext.startCall(model.address)
|
||||
return@postOnCoreThread
|
||||
}
|
||||
|
||||
val addressesCount = friend.addresses.size
|
||||
val numbersCount = friend.phoneNumbers.size
|
||||
|
||||
|
|
@ -238,12 +234,15 @@ class StartCallFragment : GenericFragment() {
|
|||
)
|
||||
|
||||
coreContext.postOnMainThread {
|
||||
val model = NumberOrAddressPickerDialogModel(list)
|
||||
val numberOrAddressModel = NumberOrAddressPickerDialogModel(list)
|
||||
val dialog =
|
||||
DialogUtils.getNumberOrAddressPickerDialog(requireActivity(), model)
|
||||
DialogUtils.getNumberOrAddressPickerDialog(
|
||||
requireActivity(),
|
||||
numberOrAddressModel
|
||||
)
|
||||
numberOrAddressPickerDialog = dialog
|
||||
|
||||
model.dismissEvent.observe(viewLifecycleOwner) { event ->
|
||||
numberOrAddressModel.dismissEvent.observe(viewLifecycleOwner) { event ->
|
||||
event.consume {
|
||||
dialog.dismiss()
|
||||
}
|
||||
|
|
|
|||
|
|
@ -22,20 +22,27 @@ package org.linphone.ui.main.calls.model
|
|||
import androidx.annotation.UiThread
|
||||
import androidx.annotation.WorkerThread
|
||||
import org.linphone.core.Address
|
||||
import org.linphone.core.Friend
|
||||
import org.linphone.ui.main.contacts.model.ContactAvatarModel
|
||||
import org.linphone.utils.LinphoneUtils
|
||||
|
||||
class SuggestionModel @WorkerThread constructor(
|
||||
class ContactOrSuggestionModel @WorkerThread constructor(
|
||||
val address: Address,
|
||||
val friend: Friend? = null,
|
||||
private val onClicked: ((Address) -> Unit)? = null
|
||||
) {
|
||||
companion object {
|
||||
private const val TAG = "[Suggestion Model]"
|
||||
}
|
||||
|
||||
val id = friend?.refKey ?: address.asStringUriOnly().hashCode()
|
||||
|
||||
val name = LinphoneUtils.getDisplayName(address)
|
||||
|
||||
val initials = LinphoneUtils.getInitials(name)
|
||||
|
||||
lateinit var contactAvatarModel: ContactAvatarModel
|
||||
|
||||
@UiThread
|
||||
fun onClicked() {
|
||||
onClicked?.invoke(address)
|
||||
|
|
@ -30,13 +30,13 @@ import kotlinx.coroutines.launch
|
|||
import org.linphone.LinphoneApplication.Companion.coreContext
|
||||
import org.linphone.LinphoneApplication.Companion.corePreferences
|
||||
import org.linphone.contacts.ContactsManager.ContactsListener
|
||||
import org.linphone.core.Address
|
||||
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.ContactOrSuggestionModel
|
||||
import org.linphone.ui.main.calls.model.NumpadModel
|
||||
import org.linphone.ui.main.calls.model.SuggestionModel
|
||||
import org.linphone.ui.main.contacts.model.ContactAvatarModel
|
||||
import org.linphone.ui.main.model.isInSecureMode
|
||||
import org.linphone.utils.Event
|
||||
|
||||
|
|
@ -47,9 +47,7 @@ class StartCallViewModel @UiThread constructor() : ViewModel() {
|
|||
|
||||
val searchFilter = MutableLiveData<String>()
|
||||
|
||||
val emptyContactsList = MutableLiveData<Boolean>()
|
||||
|
||||
val suggestionsList = MutableLiveData<ArrayList<SuggestionModel>>()
|
||||
val contactsAndSuggestionsList = MutableLiveData<ArrayList<ContactOrSuggestionModel>>()
|
||||
|
||||
val numpadModel: NumpadModel
|
||||
|
||||
|
|
@ -67,10 +65,6 @@ class StartCallViewModel @UiThread constructor() : ViewModel() {
|
|||
MutableLiveData<Event<Boolean>>()
|
||||
}
|
||||
|
||||
val onSuggestionClickedEvent: MutableLiveData<Event<Address>> by lazy {
|
||||
MutableLiveData<Event<Address>>()
|
||||
}
|
||||
|
||||
private var currentFilter = ""
|
||||
private var previousFilter = "NotSet"
|
||||
private var limitSearchToLinphoneAccounts = true
|
||||
|
|
@ -92,8 +86,8 @@ class StartCallViewModel @UiThread constructor() : ViewModel() {
|
|||
applyFilter(
|
||||
currentFilter,
|
||||
if (limitSearchToLinphoneAccounts) corePreferences.defaultDomain else "",
|
||||
MagicSearch.Source.CallLogs.toInt() or MagicSearch.Source.ChatRooms.toInt() or MagicSearch.Source.Request.toInt(),
|
||||
MagicSearch.Aggregation.None
|
||||
MagicSearch.Source.All.toInt(),
|
||||
MagicSearch.Aggregation.Friend
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -119,7 +113,7 @@ class StartCallViewModel @UiThread constructor() : ViewModel() {
|
|||
val address = core.interpretUrl(suggestion, true)
|
||||
if (address != null) {
|
||||
Log.i("$TAG Calling [${address.asStringUriOnly()}]")
|
||||
onSuggestionClickedEvent.postValue(Event(address))
|
||||
coreContext.startCall(address)
|
||||
} else {
|
||||
Log.e("$TAG Failed to parse [$suggestion] as SIP address")
|
||||
}
|
||||
|
|
@ -174,17 +168,32 @@ class StartCallViewModel @UiThread constructor() : ViewModel() {
|
|||
fun processMagicSearchResults(results: Array<SearchResult>) {
|
||||
Log.i("$TAG Processing [${results.size}] results")
|
||||
|
||||
val list = arrayListOf<SuggestionModel>()
|
||||
val contactsList = arrayListOf<ContactOrSuggestionModel>()
|
||||
val suggestionsList = arrayListOf<ContactOrSuggestionModel>()
|
||||
var previousLetter = ""
|
||||
|
||||
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 (friend != null) {
|
||||
val model = ContactOrSuggestionModel(address, friend)
|
||||
model.contactAvatarModel = ContactAvatarModel(friend)
|
||||
|
||||
val currentLetter = friend.name?.get(0).toString()
|
||||
val displayLetter = previousLetter.isEmpty() || currentLetter != previousLetter
|
||||
if (currentLetter != previousLetter) {
|
||||
previousLetter = currentLetter
|
||||
}
|
||||
model.contactAvatarModel.firstContactStartingByThatLetter.postValue(
|
||||
displayLetter
|
||||
)
|
||||
|
||||
contactsList.add(model)
|
||||
} else {
|
||||
// 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 {
|
||||
val found = suggestionsList.find {
|
||||
it.address.weakEqual(address)
|
||||
}
|
||||
if (found != null) {
|
||||
|
|
@ -195,15 +204,18 @@ class StartCallViewModel @UiThread constructor() : ViewModel() {
|
|||
}
|
||||
}
|
||||
|
||||
val model = SuggestionModel(address) {
|
||||
onSuggestionClickedEvent.value = Event(it)
|
||||
val model = ContactOrSuggestionModel(address) {
|
||||
coreContext.startCall(address)
|
||||
}
|
||||
list.add(model)
|
||||
suggestionsList.add(model)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
suggestionsList.postValue(list)
|
||||
val list = arrayListOf<ContactOrSuggestionModel>()
|
||||
list.addAll(contactsList)
|
||||
list.addAll(suggestionsList)
|
||||
contactsAndSuggestionsList.postValue(list)
|
||||
Log.i("$TAG Processed [${results.size}] results, extracted [${list.size}] suggestions")
|
||||
}
|
||||
|
||||
|
|
@ -213,8 +225,8 @@ class StartCallViewModel @UiThread constructor() : ViewModel() {
|
|||
applyFilter(
|
||||
filter,
|
||||
if (limitSearchToLinphoneAccounts) corePreferences.defaultDomain else "",
|
||||
MagicSearch.Source.CallLogs.toInt() or MagicSearch.Source.ChatRooms.toInt() or MagicSearch.Source.Request.toInt(),
|
||||
MagicSearch.Aggregation.None
|
||||
MagicSearch.Source.All.toInt(),
|
||||
MagicSearch.Aggregation.Friend
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,98 @@
|
|||
/*
|
||||
* Copyright (c) 2010-2020 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.utils
|
||||
|
||||
import android.content.Context
|
||||
import android.graphics.Canvas
|
||||
import android.graphics.Rect
|
||||
import android.util.SparseArray
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
|
||||
class RecyclerViewHeaderDecoration(private val context: Context, private val adapter: HeaderAdapter) : RecyclerView.ItemDecoration() {
|
||||
private val headers: SparseArray<View> = SparseArray()
|
||||
|
||||
override fun getItemOffsets(
|
||||
outRect: Rect,
|
||||
view: View,
|
||||
parent: RecyclerView,
|
||||
state: RecyclerView.State
|
||||
) {
|
||||
val position = (view.layoutParams as RecyclerView.LayoutParams).bindingAdapterPosition
|
||||
|
||||
if (position != RecyclerView.NO_POSITION && adapter.displayHeaderForPosition(position)) {
|
||||
val headerView: View = adapter.getHeaderViewForPosition(view.context, position)
|
||||
headers.put(position, headerView)
|
||||
measureHeaderView(headerView, parent)
|
||||
outRect.top = headerView.height
|
||||
} else {
|
||||
headers.remove(position)
|
||||
}
|
||||
}
|
||||
|
||||
private fun measureHeaderView(view: View, parent: ViewGroup) {
|
||||
if (view.layoutParams == null) {
|
||||
view.layoutParams = ViewGroup.LayoutParams(
|
||||
ViewGroup.LayoutParams.MATCH_PARENT,
|
||||
ViewGroup.LayoutParams.WRAP_CONTENT
|
||||
)
|
||||
}
|
||||
|
||||
val widthSpec = View.MeasureSpec.makeMeasureSpec(parent.width, View.MeasureSpec.EXACTLY)
|
||||
val heightSpec = View.MeasureSpec.makeMeasureSpec(parent.height, View.MeasureSpec.EXACTLY)
|
||||
val childWidth = ViewGroup.getChildMeasureSpec(
|
||||
widthSpec,
|
||||
parent.paddingLeft + parent.paddingRight,
|
||||
view.layoutParams.width
|
||||
)
|
||||
val childHeight = ViewGroup.getChildMeasureSpec(
|
||||
heightSpec,
|
||||
parent.paddingTop + parent.paddingBottom,
|
||||
view.layoutParams.height
|
||||
)
|
||||
|
||||
view.measure(childWidth, childHeight)
|
||||
view.layout(0, 0, view.measuredWidth, view.measuredHeight)
|
||||
}
|
||||
|
||||
override fun onDraw(canvas: Canvas, parent: RecyclerView, state: RecyclerView.State) {
|
||||
for (i in 0 until parent.childCount) {
|
||||
val child = parent.getChildAt(i)
|
||||
val position = parent.getChildAdapterPosition(child)
|
||||
if (position != RecyclerView.NO_POSITION && adapter.displayHeaderForPosition(position)) {
|
||||
canvas.save()
|
||||
val headerView: View = headers.get(position) ?: adapter.getHeaderViewForPosition(
|
||||
context,
|
||||
position
|
||||
)
|
||||
canvas.translate(0f, child.y - headerView.height)
|
||||
headerView.draw(canvas)
|
||||
canvas.restore()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
interface HeaderAdapter {
|
||||
fun displayHeaderForPosition(position: Int): Boolean
|
||||
|
||||
fun getHeaderViewForPosition(context: Context, position: Int): View
|
||||
}
|
||||
|
|
@ -251,7 +251,7 @@
|
|||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="32dp"
|
||||
android:layout_marginBottom="32dp"
|
||||
android:layout_marginBottom="@dimen/screen_bottom_margin"
|
||||
android:layout_marginStart="16dp"
|
||||
android:layout_marginEnd="16dp"
|
||||
android:paddingStart="20dp"
|
||||
|
|
|
|||
|
|
@ -280,7 +280,7 @@
|
|||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="40dp"
|
||||
android:layout_marginStart="20dp"
|
||||
android:layout_marginBottom="32dp"
|
||||
android:layout_marginBottom="@dimen/screen_bottom_margin"
|
||||
android:paddingStart="20dp"
|
||||
android:paddingEnd="20dp"
|
||||
android:text="@string/assistant_account_register"
|
||||
|
|
|
|||
|
|
@ -300,7 +300,7 @@
|
|||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="34dp"
|
||||
android:layout_marginStart="20dp"
|
||||
android:layout_marginBottom="32dp"
|
||||
android:layout_marginBottom="@dimen/screen_bottom_margin"
|
||||
android:paddingStart="20dp"
|
||||
android:paddingEnd="20dp"
|
||||
android:text="@string/assistant_account_login"
|
||||
|
|
|
|||
|
|
@ -266,7 +266,7 @@
|
|||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="32dp"
|
||||
android:layout_marginBottom="32dp"
|
||||
android:layout_marginBottom="@dimen/screen_bottom_margin"
|
||||
android:layout_marginStart="16dp"
|
||||
android:layout_marginEnd="16dp"
|
||||
android:paddingStart="20dp"
|
||||
|
|
|
|||
|
|
@ -234,7 +234,7 @@
|
|||
android:layout_marginTop="32dp"
|
||||
android:layout_marginStart="16dp"
|
||||
android:layout_marginEnd="16dp"
|
||||
android:layout_marginBottom="32dp"
|
||||
android:layout_marginBottom="@dimen/screen_bottom_margin"
|
||||
android:paddingStart="20dp"
|
||||
android:paddingEnd="20dp"
|
||||
android:text="@string/assistant_account_login"
|
||||
|
|
|
|||
|
|
@ -147,7 +147,7 @@
|
|||
android:layout_marginTop="32dp"
|
||||
android:layout_marginStart="16dp"
|
||||
android:layout_marginEnd="16dp"
|
||||
android:layout_marginBottom="32dp"
|
||||
android:layout_marginBottom="@dimen/screen_bottom_margin"
|
||||
android:paddingStart="20dp"
|
||||
android:paddingEnd="20dp"
|
||||
android:text="@string/assistant_third_party_sip_account_warning_ok"
|
||||
|
|
|
|||
|
|
@ -81,6 +81,8 @@
|
|||
android:paddingBottom="10dp"
|
||||
android:text="@={viewModel.searchFilter}"
|
||||
android:textSize="14sp"
|
||||
app:layout_constraintHeight_min="48dp"
|
||||
app:layout_constraintWidth_max="@dimen/text_input_max_width"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@id/title" />
|
||||
|
|
@ -159,17 +161,17 @@
|
|||
<ImageView
|
||||
android:id="@+id/no_contacts_nor_suggestion_image"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_height="0dp"
|
||||
android:layout_margin="10dp"
|
||||
android:src="@drawable/illu"
|
||||
android:visibility="@{viewModel.emptyContactsList && viewModel.suggestionsList.size() == 0 ? View.VISIBLE : View.GONE}"
|
||||
app:layout_constraintBottom_toTopOf="@id/no_contacts_nor_suggestion_label"
|
||||
android:visibility="@{viewModel.contactsAndSuggestionsList.size() == 0 ? View.VISIBLE : View.GONE}"
|
||||
app:layout_constraintDimensionRatio="1:1"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintHeight_max="200dp"
|
||||
app:layout_constraintVertical_bias="0.3"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@id/group_call_icon"
|
||||
app:layout_constraintVertical_chainStyle="packed" />
|
||||
app:layout_constraintBottom_toBottomOf="parent" />
|
||||
|
||||
<androidx.appcompat.widget.AppCompatTextView
|
||||
style="@style/section_header_style"
|
||||
|
|
@ -178,82 +180,39 @@
|
|||
android:layout_height="wrap_content"
|
||||
android:text="@string/call_start_no_suggestion_nor_contact"
|
||||
android:gravity="center"
|
||||
android:visibility="@{viewModel.emptyContactsList && viewModel.suggestionsList.size() == 0 ? View.VISIBLE : View.GONE}"
|
||||
android:visibility="@{viewModel.contactsAndSuggestionsList.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" />
|
||||
|
||||
<androidx.appcompat.widget.AppCompatTextView
|
||||
style="@style/section_header_style"
|
||||
android:id="@+id/all_contacts_label"
|
||||
android:id="@+id/contacts_and_suggestions_label"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="16dp"
|
||||
android:layout_marginTop="16dp"
|
||||
android:layout_marginEnd="16dp"
|
||||
android:padding="5dp"
|
||||
android:text="@string/contacts_list_title"
|
||||
android:visibility="@{viewModel.emptyContactsList ? View.GONE : View.VISIBLE}"
|
||||
android:text="@string/call_start_contacts_list_title"
|
||||
android:visibility="@{viewModel.contactsAndSuggestionsList.size() == 0 ? 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"/>
|
||||
app:layout_constraintBottom_toTopOf="@id/contacts_and_suggestions_list"/>
|
||||
|
||||
<androidx.recyclerview.widget.RecyclerView
|
||||
android:id="@+id/contacts_list"
|
||||
android:id="@+id/contacts_and_suggestions_list"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_height="0dp"
|
||||
android:layout_marginTop="8dp"
|
||||
android:visibility="@{viewModel.emptyContactsList ? View.GONE : View.VISIBLE}"
|
||||
app:layout_constrainedHeight="true"
|
||||
android:visibility="@{viewModel.contactsAndSuggestionsList.size() == 0 ? View.GONE : View.VISIBLE}"
|
||||
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.constraintlayout.widget.Barrier
|
||||
android:id="@+id/suggestions_top_barrier"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
app:barrierDirection="bottom"
|
||||
app:constraint_referenced_ids="contacts_list, all_contacts_label, group_call_icon" />
|
||||
|
||||
<androidx.appcompat.widget.AppCompatTextView
|
||||
style="@style/section_header_style"
|
||||
android:id="@+id/suggestions_label"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="16dp"
|
||||
android:layout_marginTop="16dp"
|
||||
android:layout_marginEnd="16dp"
|
||||
android:padding="5dp"
|
||||
android:text="@string/call_start_suggestions_list_title"
|
||||
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/suggestions_top_barrier"
|
||||
app:layout_constraintBottom_toTopOf="@id/suggestions_list"/>
|
||||
|
||||
<ScrollView
|
||||
android:id="@+id/suggestions_list"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
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">
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="vertical"
|
||||
entries="@{viewModel.suggestionsList}"
|
||||
layout="@{@layout/call_suggestion_list_cell}" />
|
||||
|
||||
</ScrollView>
|
||||
app:layout_constraintTop_toBottomOf="@id/contacts_and_suggestions_label"
|
||||
app:layout_constraintBottom_toBottomOf="parent" />
|
||||
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||
|
||||
|
|
|
|||
|
|
@ -5,13 +5,16 @@
|
|||
|
||||
<data>
|
||||
<import type="android.view.View" />
|
||||
<variable
|
||||
name="onClickListener"
|
||||
type="View.OnClickListener" />
|
||||
<variable
|
||||
name="model"
|
||||
type="org.linphone.ui.main.calls.model.SuggestionModel" />
|
||||
type="org.linphone.ui.main.calls.model.ContactOrSuggestionModel" />
|
||||
</data>
|
||||
|
||||
<androidx.constraintlayout.widget.ConstraintLayout
|
||||
android:onClick="@{() -> model.onClicked()}"
|
||||
android:onClick="@{onClickListener}"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:background="@drawable/primary_cell_background"
|
||||
|
|
|
|||
|
|
@ -0,0 +1,9 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<androidx.appcompat.widget.AppCompatTextView xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
style="@style/section_header_style"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:paddingStart="21dp"
|
||||
android:paddingTop="16dp"
|
||||
android:paddingBottom="16dp"
|
||||
android:text="@string/call_start_suggestions_list_title"/>
|
||||
|
|
@ -23,7 +23,6 @@
|
|||
android:onLongClick="@{onLongClickListener}"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:background="@color/white"
|
||||
android:layout_marginStart="4dp"
|
||||
android:layout_marginEnd="16dp">
|
||||
|
||||
|
|
|
|||
|
|
@ -257,7 +257,7 @@
|
|||
android:layout_marginTop="5dp"
|
||||
android:paddingStart="20dp"
|
||||
android:paddingEnd="20dp"
|
||||
android:layout_marginBottom="32dp"
|
||||
android:layout_marginBottom="@dimen/screen_bottom_margin"
|
||||
android:text="@={viewModel.jobTitle, default=`Android dev`}"
|
||||
android:textSize="14sp"
|
||||
android:textColor="@color/gray_9"
|
||||
|
|
|
|||
|
|
@ -492,7 +492,7 @@
|
|||
android:layout_marginStart="26dp"
|
||||
android:layout_marginEnd="26dp"
|
||||
android:layout_marginTop="16dp"
|
||||
android:layout_marginBottom="32dp"
|
||||
android:layout_marginBottom="@dimen/screen_bottom_margin"
|
||||
android:text="@string/settings_advanced_title"
|
||||
android:drawableEnd="@drawable/caret_right"
|
||||
android:drawableTint="@color/gray_9"
|
||||
|
|
|
|||
|
|
@ -2,6 +2,8 @@
|
|||
<resources>
|
||||
<dimen name="zero">0dp</dimen>
|
||||
|
||||
<dimen name="screen_bottom_margin">32dp</dimen>
|
||||
|
||||
<dimen name="landscape_nav_bar_width">75dp</dimen>
|
||||
<dimen name="sliding_pane_left_fragment_width">400dp</dimen>
|
||||
<!-- This value is the result of the above two added together -->
|
||||
|
|
|
|||
|
|
@ -208,6 +208,7 @@
|
|||
<string name="call_start_search_bar_filter_hint">Search contact or history call</string>
|
||||
<string name="call_start_create_group_call">Create a group call</string>
|
||||
<string name="call_start_no_suggestion_nor_contact">No suggestion and no contact for the moment…</string>
|
||||
<string name="call_start_contacts_list_title">Contacts</string>
|
||||
<string name="call_start_suggestions_list_title">Suggestions</string>
|
||||
|
||||
<string name="calls_list_empty_history">No call for the moment…</string>
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue