Reimplement contact chips with a recyclerview.

This commit is contained in:
Alex Hart 2022-06-29 09:10:08 -03:00 committed by Cody Henthorne
parent 4215b0391d
commit 7bd34d2b99
6 changed files with 191 additions and 118 deletions

View file

@ -18,7 +18,6 @@ package org.thoughtcrime.securesms;
import android.Manifest; import android.Manifest;
import android.animation.LayoutTransition;
import android.annotation.SuppressLint; import android.annotation.SuppressLint;
import android.content.Context; import android.content.Context;
import android.content.Intent; import android.content.Intent;
@ -32,7 +31,6 @@ import android.view.View;
import android.view.ViewGroup; import android.view.ViewGroup;
import android.view.WindowManager; import android.view.WindowManager;
import android.widget.Button; import android.widget.Button;
import android.widget.HorizontalScrollView;
import android.widget.TextView; import android.widget.TextView;
import android.widget.Toast; import android.widget.Toast;
@ -43,6 +41,7 @@ import androidx.appcompat.app.AlertDialog;
import androidx.constraintlayout.widget.ConstraintLayout; import androidx.constraintlayout.widget.ConstraintLayout;
import androidx.constraintlayout.widget.ConstraintSet; import androidx.constraintlayout.widget.ConstraintSet;
import androidx.fragment.app.FragmentActivity; import androidx.fragment.app.FragmentActivity;
import androidx.lifecycle.ViewModelProvider;
import androidx.loader.app.LoaderManager; import androidx.loader.app.LoaderManager;
import androidx.loader.content.Loader; import androidx.loader.content.Loader;
import androidx.recyclerview.widget.DefaultItemAnimator; import androidx.recyclerview.widget.DefaultItemAnimator;
@ -54,7 +53,6 @@ import androidx.transition.TransitionManager;
import com.annimon.stream.Collectors; import com.annimon.stream.Collectors;
import com.annimon.stream.Stream; import com.annimon.stream.Stream;
import com.google.android.material.chip.ChipGroup;
import com.google.android.material.dialog.MaterialAlertDialogBuilder; import com.google.android.material.dialog.MaterialAlertDialogBuilder;
import com.pnikosis.materialishprogress.ProgressWheel; import com.pnikosis.materialishprogress.ProgressWheel;
@ -62,7 +60,7 @@ import org.signal.core.util.concurrent.SimpleTask;
import org.signal.core.util.logging.Log; import org.signal.core.util.logging.Log;
import org.thoughtcrime.securesms.components.RecyclerViewFastScroller; import org.thoughtcrime.securesms.components.RecyclerViewFastScroller;
import org.thoughtcrime.securesms.contacts.AbstractContactsCursorLoader; import org.thoughtcrime.securesms.contacts.AbstractContactsCursorLoader;
import org.thoughtcrime.securesms.contacts.ContactChip; import org.thoughtcrime.securesms.contacts.ContactChipViewModel;
import org.thoughtcrime.securesms.contacts.ContactSelectionListAdapter; import org.thoughtcrime.securesms.contacts.ContactSelectionListAdapter;
import org.thoughtcrime.securesms.contacts.ContactSelectionListItem; import org.thoughtcrime.securesms.contacts.ContactSelectionListItem;
import org.thoughtcrime.securesms.contacts.ContactsCursorLoader; import org.thoughtcrime.securesms.contacts.ContactsCursorLoader;
@ -70,6 +68,7 @@ import org.thoughtcrime.securesms.contacts.ContactsCursorLoader.DisplayMode;
import org.thoughtcrime.securesms.contacts.HeaderAction; import org.thoughtcrime.securesms.contacts.HeaderAction;
import org.thoughtcrime.securesms.contacts.LetterHeaderDecoration; import org.thoughtcrime.securesms.contacts.LetterHeaderDecoration;
import org.thoughtcrime.securesms.contacts.SelectedContact; import org.thoughtcrime.securesms.contacts.SelectedContact;
import org.thoughtcrime.securesms.contacts.SelectedContacts;
import org.thoughtcrime.securesms.contacts.selection.ContactSelectionArguments; import org.thoughtcrime.securesms.contacts.selection.ContactSelectionArguments;
import org.thoughtcrime.securesms.contacts.sync.ContactDiscovery; import org.thoughtcrime.securesms.contacts.sync.ContactDiscovery;
import org.thoughtcrime.securesms.groups.SelectionLimits; import org.thoughtcrime.securesms.groups.SelectionLimits;
@ -77,15 +76,17 @@ import org.thoughtcrime.securesms.groups.ui.GroupLimitDialog;
import org.thoughtcrime.securesms.mms.GlideApp; import org.thoughtcrime.securesms.mms.GlideApp;
import org.thoughtcrime.securesms.mms.GlideRequests; import org.thoughtcrime.securesms.mms.GlideRequests;
import org.thoughtcrime.securesms.permissions.Permissions; import org.thoughtcrime.securesms.permissions.Permissions;
import org.thoughtcrime.securesms.recipients.LiveRecipient;
import org.thoughtcrime.securesms.recipients.Recipient; import org.thoughtcrime.securesms.recipients.Recipient;
import org.thoughtcrime.securesms.recipients.RecipientId; import org.thoughtcrime.securesms.recipients.RecipientId;
import org.thoughtcrime.securesms.sharing.ShareContact; import org.thoughtcrime.securesms.sharing.ShareContact;
import org.thoughtcrime.securesms.util.LifecycleDisposable;
import org.thoughtcrime.securesms.util.TextSecurePreferences; import org.thoughtcrime.securesms.util.TextSecurePreferences;
import org.thoughtcrime.securesms.util.UsernameUtil; import org.thoughtcrime.securesms.util.UsernameUtil;
import org.thoughtcrime.securesms.util.ViewUtil; import org.thoughtcrime.securesms.util.ViewUtil;
import org.thoughtcrime.securesms.util.adapter.FixedViewsAdapter; import org.thoughtcrime.securesms.util.adapter.FixedViewsAdapter;
import org.thoughtcrime.securesms.util.adapter.RecyclerViewConcatenateAdapterStickyHeader; import org.thoughtcrime.securesms.util.adapter.RecyclerViewConcatenateAdapterStickyHeader;
import org.thoughtcrime.securesms.util.adapter.mapping.MappingAdapter;
import org.thoughtcrime.securesms.util.adapter.mapping.MappingModelList;
import org.thoughtcrime.securesms.util.views.SimpleProgressDialog; import org.thoughtcrime.securesms.util.views.SimpleProgressDialog;
import java.io.IOException; import java.io.IOException;
@ -95,6 +96,10 @@ import java.util.Optional;
import java.util.Set; import java.util.Set;
import java.util.function.Consumer; import java.util.function.Consumer;
import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers;
import io.reactivex.rxjava3.disposables.Disposable;
import kotlin.Unit;
/** /**
* Fragment for selecting a one or more contacts from a list. * Fragment for selecting a one or more contacts from a list.
* *
@ -106,7 +111,7 @@ public final class ContactSelectionListFragment extends LoggingFragment
@SuppressWarnings("unused") @SuppressWarnings("unused")
private static final String TAG = Log.tag(ContactSelectionListFragment.class); private static final String TAG = Log.tag(ContactSelectionListFragment.class);
private static final int CHIP_GROUP_EMPTY_CHILD_COUNT = 1; private static final int CHIP_GROUP_EMPTY_CHILD_COUNT = 0;
private static final int CHIP_GROUP_REVEAL_DURATION_MS = 150; private static final int CHIP_GROUP_REVEAL_DURATION_MS = 150;
public static final int NO_LIMIT = Integer.MAX_VALUE; public static final int NO_LIMIT = Integer.MAX_VALUE;
@ -134,10 +139,12 @@ public final class ContactSelectionListFragment extends LoggingFragment
private RecyclerView recyclerView; private RecyclerView recyclerView;
private RecyclerViewFastScroller fastScroller; private RecyclerViewFastScroller fastScroller;
private ContactSelectionListAdapter cursorRecyclerViewAdapter; private ContactSelectionListAdapter cursorRecyclerViewAdapter;
private ChipGroup chipGroup; private RecyclerView chipRecycler;
private HorizontalScrollView chipGroupScrollContainer;
private OnSelectionLimitReachedListener onSelectionLimitReachedListener; private OnSelectionLimitReachedListener onSelectionLimitReachedListener;
private AbstractContactsCursorLoaderFactoryProvider cursorFactoryProvider; private AbstractContactsCursorLoaderFactoryProvider cursorFactoryProvider;
private MappingAdapter contactChipAdapter;
private ContactChipViewModel contactChipViewModel;
private LifecycleDisposable lifecycleDisposable;
private HeaderActionProvider headerActionProvider; private HeaderActionProvider headerActionProvider;
private TextView headerActionView; private TextView headerActionView;
@ -249,8 +256,7 @@ public final class ContactSelectionListFragment extends LoggingFragment
showContactsButton = view.findViewById(R.id.show_contacts_button); showContactsButton = view.findViewById(R.id.show_contacts_button);
showContactsDescription = view.findViewById(R.id.show_contacts_description); showContactsDescription = view.findViewById(R.id.show_contacts_description);
showContactsProgress = view.findViewById(R.id.progress); showContactsProgress = view.findViewById(R.id.progress);
chipGroup = view.findViewById(R.id.chipGroup); chipRecycler = view.findViewById(R.id.chipRecycler);
chipGroupScrollContainer = view.findViewById(R.id.chipGroupScrollContainer);
constraintLayout = view.findViewById(R.id.container); constraintLayout = view.findViewById(R.id.container);
headerActionView = view.findViewById(R.id.header_action); headerActionView = view.findViewById(R.id.header_action);
@ -264,6 +270,18 @@ public final class ContactSelectionListFragment extends LoggingFragment
} }
}); });
contactChipViewModel = new ViewModelProvider(this).get(ContactChipViewModel.class);
contactChipAdapter = new MappingAdapter();
lifecycleDisposable = new LifecycleDisposable();
lifecycleDisposable.bindTo(getViewLifecycleOwner());
SelectedContacts.register(contactChipAdapter, this::onChipCloseIconClicked);
chipRecycler.setAdapter(contactChipAdapter);
Disposable disposable = contactChipViewModel.getState().subscribe(this::handleSelectedContactsChanged);
lifecycleDisposable.add(disposable);
Intent intent = requireActivity().getIntent(); Intent intent = requireActivity().getIntent();
Bundle arguments = safeArguments(); Bundle arguments = safeArguments();
@ -730,75 +748,22 @@ public final class ContactSelectionListFragment extends LoggingFragment
private void markContactUnselected(@NonNull SelectedContact selectedContact) { private void markContactUnselected(@NonNull SelectedContact selectedContact) {
cursorRecyclerViewAdapter.removeFromSelectedContacts(selectedContact); cursorRecyclerViewAdapter.removeFromSelectedContacts(selectedContact);
cursorRecyclerViewAdapter.notifyItemRangeChanged(0, cursorRecyclerViewAdapter.getItemCount(), ContactSelectionListAdapter.PAYLOAD_SELECTION_CHANGE); cursorRecyclerViewAdapter.notifyItemRangeChanged(0, cursorRecyclerViewAdapter.getItemCount(), ContactSelectionListAdapter.PAYLOAD_SELECTION_CHANGE);
removeChipForContact(selectedContact); contactChipViewModel.remove(selectedContact);
if (onContactSelectedListener != null) { if (onContactSelectedListener != null) {
onContactSelectedListener.onSelectionChanged(); onContactSelectedListener.onSelectionChanged();
} }
} }
private void removeChipForContact(@NonNull SelectedContact contact) { private void handleSelectedContactsChanged(@NonNull List<SelectedContacts.Model> selectedContacts) {
for (int i = chipGroup.getChildCount() - 1; i >= 0; i--) { contactChipAdapter.submitList(new MappingModelList(selectedContacts), this::smoothScrollChipsToEnd);
View v = chipGroup.getChildAt(i);
if (v instanceof ContactChip && contact.matches(((ContactChip) v).getContact())) {
chipGroup.removeView(v);
}
}
if (getChipCount() == 0) { if (selectedContacts.isEmpty()) {
setChipGroupVisibility(ConstraintSet.GONE); setChipGroupVisibility(ConstraintSet.GONE);
} } else {
}
private void addChipForSelectedContact(@NonNull SelectedContact selectedContact) {
SimpleTask.run(getViewLifecycleOwner().getLifecycle(),
() -> Recipient.resolved(selectedContact.getOrCreateRecipientId(requireContext())),
resolved -> addChipForRecipient(resolved, selectedContact));
}
private void addChipForRecipient(@NonNull Recipient recipient, @NonNull SelectedContact selectedContact) {
final ContactChip chip = new ContactChip(requireContext());
if (getChipCount() == 0) {
setChipGroupVisibility(ConstraintSet.VISIBLE); setChipGroupVisibility(ConstraintSet.VISIBLE);
} }
chip.setText(recipient.getShortDisplayName(requireContext()));
chip.setContact(selectedContact);
chip.setCloseIconVisible(true);
chip.setOnCloseIconClickListener(view -> {
markContactUnselected(selectedContact);
if (onContactSelectedListener != null) {
onContactSelectedListener.onContactDeselected(Optional.of(recipient.getId()), recipient.getE164().orElse(null));
}
});
chipGroup.getLayoutTransition().addTransitionListener(new LayoutTransition.TransitionListener() {
@Override
public void startTransition(LayoutTransition transition, ViewGroup container, View view, int transitionType) {
}
@Override
public void endTransition(LayoutTransition transition, ViewGroup container, View view, int transitionType) {
if (getView() == null || !requireView().isAttachedToWindow()) {
Log.w(TAG, "Fragment's view was detached before the animation completed.");
return;
}
if (view == chip && transitionType == LayoutTransition.APPEARING) {
chipGroup.getLayoutTransition().removeTransitionListener(this);
registerChipRecipientObserver(chip, recipient.live());
chipGroup.post(ContactSelectionListFragment.this::smoothScrollChipsToEnd);
}
}
});
chip.setAvatar(glideRequests, recipient, () -> addChip(chip));
}
private void addChip(@NonNull ContactChip chip) {
chipGroup.addView(chip);
if (selectionWarningLimitReachedExactly()) { if (selectionWarningLimitReachedExactly()) {
if (onSelectionLimitReachedListener != null) { if (onSelectionLimitReachedListener != null) {
onSelectionLimitReachedListener.onSuggestedLimitReached(selectionLimit.getRecommendedLimit()); onSelectionLimitReachedListener.onSuggestedLimitReached(selectionLimit.getRecommendedLimit());
@ -808,21 +773,25 @@ public final class ContactSelectionListFragment extends LoggingFragment
} }
} }
private int getChipCount() { private void addChipForSelectedContact(@NonNull SelectedContact selectedContact) {
int count = chipGroup.getChildCount() - CHIP_GROUP_EMPTY_CHILD_COUNT; SimpleTask.run(getViewLifecycleOwner().getLifecycle(),
if (count < 0) throw new AssertionError(); () -> Recipient.resolved(selectedContact.getOrCreateRecipientId(requireContext())),
return count; resolved -> contactChipViewModel.add(selectedContact));
} }
private void registerChipRecipientObserver(@NonNull ContactChip chip, @Nullable LiveRecipient recipient) { private Unit onChipCloseIconClicked(SelectedContacts.Model model) {
if (recipient != null) { markContactUnselected(model.getSelectedContact());
recipient.observe(getViewLifecycleOwner(), resolved -> { if (onContactSelectedListener != null) {
if (chip.isAttachedToWindow()) { onContactSelectedListener.onContactDeselected(Optional.of(model.getRecipient().getId()), model.getRecipient().getE164().orElse(null));
chip.setAvatar(glideRequests, resolved, null);
chip.setText(resolved.getShortDisplayName(chip.getContext()));
}
});
} }
return Unit.INSTANCE;
}
private int getChipCount() {
int count = contactChipViewModel.getCount() - CHIP_GROUP_EMPTY_CHILD_COUNT;
if (count < 0) throw new AssertionError();
return count;
} }
private void setChipGroupVisibility(int visibility) { private void setChipGroupVisibility(int visibility) {
@ -838,7 +807,7 @@ public final class ContactSelectionListFragment extends LoggingFragment
ConstraintSet constraintSet = new ConstraintSet(); ConstraintSet constraintSet = new ConstraintSet();
constraintSet.clone(constraintLayout); constraintSet.clone(constraintLayout);
constraintSet.setVisibility(R.id.chipGroupScrollContainer, visibility); constraintSet.setVisibility(R.id.chipRecycler, visibility);
constraintSet.applyTo(constraintLayout); constraintSet.applyTo(constraintLayout);
} }
@ -847,8 +816,8 @@ public final class ContactSelectionListFragment extends LoggingFragment
} }
private void smoothScrollChipsToEnd() { private void smoothScrollChipsToEnd() {
int x = ViewUtil.isLtr(chipGroupScrollContainer) ? chipGroup.getWidth() : 0; int x = ViewUtil.isLtr(chipRecycler) ? chipRecycler.getWidth() : 0;
chipGroupScrollContainer.smoothScrollTo(x, 0); chipRecycler.smoothScrollBy(x, 0);
} }
public interface OnContactSelectedListener { public interface OnContactSelectedListener {

View file

@ -0,0 +1,72 @@
package org.thoughtcrime.securesms.contacts
import androidx.lifecycle.ViewModel
import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers
import io.reactivex.rxjava3.core.BackpressureStrategy
import io.reactivex.rxjava3.core.Flowable
import io.reactivex.rxjava3.core.Single
import io.reactivex.rxjava3.disposables.CompositeDisposable
import io.reactivex.rxjava3.disposables.Disposable
import io.reactivex.rxjava3.kotlin.plusAssign
import io.reactivex.rxjava3.schedulers.Schedulers
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies
import org.thoughtcrime.securesms.recipients.Recipient
import org.thoughtcrime.securesms.recipients.RecipientId
import org.thoughtcrime.securesms.util.rx.RxStore
/**
* ViewModel expressly for displaying the current state of the contact chips
* in the contact selection fragment.
*/
class ContactChipViewModel : ViewModel() {
private val store = RxStore(emptyList<SelectedContacts.Model>())
val state: Flowable<List<SelectedContacts.Model>> = store.stateFlowable
.distinctUntilChanged()
.observeOn(AndroidSchedulers.mainThread())
val count = store.state.size
private val disposables = CompositeDisposable()
private val disposableMap: MutableMap<RecipientId, Disposable> = mutableMapOf()
override fun onCleared() {
disposables.clear()
disposableMap.values.forEach { it.dispose() }
disposableMap.clear()
}
fun add(selectedContact: SelectedContact) {
disposables += getOrCreateRecipientId(selectedContact).map { Recipient.resolved(it) }.observeOn(Schedulers.io()).subscribe { recipient ->
store.update { it + SelectedContacts.Model(selectedContact, recipient) }
disposableMap[recipient.id]?.dispose()
disposableMap[recipient.id] = store.update(recipient.live().asObservable().toFlowable(BackpressureStrategy.LATEST)) { changedRecipient, state ->
val index = state.indexOfFirst { it.selectedContact.matches(selectedContact) }
when {
index == 0 -> {
listOf(SelectedContacts.Model(selectedContact, changedRecipient)) + state.drop(index + 1)
}
index > 0 -> {
state.take(index) + SelectedContacts.Model(selectedContact, changedRecipient) + state.drop(index + 1)
}
else -> {
state
}
}
}
}
}
fun remove(selectedContact: SelectedContact) {
store.update { list ->
list.filterNot { it.selectedContact.matches(selectedContact) }
}
}
private fun getOrCreateRecipientId(selectedContact: SelectedContact): Single<RecipientId> {
return Single.fromCallable {
selectedContact.getOrCreateRecipientId(ApplicationDependencies.getApplication())
}
}
}

View file

@ -0,0 +1,42 @@
package org.thoughtcrime.securesms.contacts
import android.view.View
import org.thoughtcrime.securesms.R
import org.thoughtcrime.securesms.mms.GlideApp
import org.thoughtcrime.securesms.recipients.Recipient
import org.thoughtcrime.securesms.util.adapter.mapping.LayoutFactory
import org.thoughtcrime.securesms.util.adapter.mapping.MappingAdapter
import org.thoughtcrime.securesms.util.adapter.mapping.MappingModel
import org.thoughtcrime.securesms.util.adapter.mapping.MappingViewHolder
object SelectedContacts {
@JvmStatic
fun register(adapter: MappingAdapter, onCloseIconClicked: (Model) -> Unit) {
adapter.registerFactory(Model::class.java, LayoutFactory({ ViewHolder(it, onCloseIconClicked) }, R.layout.contact_selection_list_chip))
}
class Model(val selectedContact: SelectedContact, val recipient: Recipient) : MappingModel<Model> {
override fun areItemsTheSame(newItem: Model): Boolean {
return newItem.selectedContact.matches(selectedContact) && recipient == newItem.recipient
}
override fun areContentsTheSame(newItem: Model): Boolean {
return areItemsTheSame(newItem) && recipient.hasSameContent(newItem.recipient)
}
}
private class ViewHolder(itemView: View, private val onCloseIconClicked: (Model) -> Unit) : MappingViewHolder<Model>(itemView) {
private val chip: ContactChip = itemView.findViewById(R.id.contact_chip)
override fun bind(model: Model) {
chip.text = model.recipient.getShortDisplayName(context)
chip.setContact(model.selectedContact)
chip.isCloseIconVisible = true
chip.setOnCloseIconClickListener {
onCloseIconClicked(model)
}
chip.setAvatar(GlideApp.with(itemView), model.recipient, null)
}
}
}

View file

@ -2,6 +2,7 @@ package org.thoughtcrime.securesms.util.rx
import io.reactivex.rxjava3.core.Flowable import io.reactivex.rxjava3.core.Flowable
import io.reactivex.rxjava3.core.Scheduler import io.reactivex.rxjava3.core.Scheduler
import io.reactivex.rxjava3.disposables.Disposable
import io.reactivex.rxjava3.processors.BehaviorProcessor import io.reactivex.rxjava3.processors.BehaviorProcessor
import io.reactivex.rxjava3.schedulers.Schedulers import io.reactivex.rxjava3.schedulers.Schedulers
import io.reactivex.rxjava3.subjects.PublishSubject import io.reactivex.rxjava3.subjects.PublishSubject
@ -31,4 +32,10 @@ class RxStore<T : Any>(
fun update(transformer: (T) -> T) { fun update(transformer: (T) -> T) {
actionSubject.onNext(transformer) actionSubject.onNext(transformer)
} }
fun <U> update(flowable: Flowable<U>, transformer: (U, T) -> T): Disposable {
return flowable.subscribe {
actionSubject.onNext { t -> transformer(it, t) }
}
}
} }

View file

@ -0,0 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<org.thoughtcrime.securesms.contacts.ContactChip xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/contact_chip"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginEnd="12dp" />

View file

@ -14,7 +14,7 @@
app:layout_constraintEnd_toEndOf="parent" app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintHorizontal_bias="0.5" app:layout_constraintHorizontal_bias="0.5"
app:layout_constraintStart_toStartOf="parent" app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/chipGroupScrollContainer"> app:layout_constraintTop_toBottomOf="@+id/chipRecycler">
<androidx.recyclerview.widget.RecyclerView <androidx.recyclerview.widget.RecyclerView
android:id="@+id/recycler_view" android:id="@+id/recycler_view"
@ -44,7 +44,7 @@
android:visibility="gone" android:visibility="gone"
app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent" app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toBottomOf="@+id/chipGroupScrollContainer" app:layout_constraintTop_toBottomOf="@+id/chipRecycler"
tools:visibility="visible" /> tools:visibility="visible" />
<LinearLayout <LinearLayout
@ -58,12 +58,12 @@
app:layout_constraintEnd_toEndOf="parent" app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintHorizontal_bias="0.5" app:layout_constraintHorizontal_bias="0.5"
app:layout_constraintStart_toStartOf="parent" app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/chipGroupScrollContainer" app:layout_constraintTop_toBottomOf="@+id/chipRecycler"
tools:visibility="visible"> tools:visibility="visible">
<com.google.android.material.button.MaterialButton <com.google.android.material.button.MaterialButton
style="@style/Signal.Widget.Button.Large.Primary"
android:id="@+id/show_contacts_button" android:id="@+id/show_contacts_button"
style="@style/Signal.Widget.Button.Large.Primary"
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_gravity="center_horizontal" android:layout_gravity="center_horizontal"
@ -112,45 +112,22 @@
</LinearLayout> </LinearLayout>
<HorizontalScrollView <androidx.recyclerview.widget.RecyclerView
android:id="@+id/chipGroupScrollContainer" android:id="@+id/chipRecycler"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="56dp" android:layout_height="wrap_content"
android:clipChildren="false" android:clipChildren="false"
android:clipToPadding="false" android:clipToPadding="false"
android:minHeight="48sp"
android:orientation="horizontal"
android:paddingStart="@dimen/dsl_settings_gutter" android:paddingStart="@dimen/dsl_settings_gutter"
android:paddingEnd="@dimen/dsl_settings_gutter" android:paddingEnd="@dimen/dsl_settings_gutter"
android:scrollbars="none" android:scrollbars="none"
android:visibility="gone" android:visibility="gone"
app:layout_constraintEnd_toEndOf="parent" app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager"
app:layout_constraintHorizontal_bias="0.5"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" app:layout_constraintTop_toTopOf="parent"
tools:visibility="visible"> tools:listitem="@layout/contact_selection_list_chip"
tools:visibility="visible" />
<FrameLayout
android:layout_width="wrap_content"
android:layout_height="match_parent">
<com.google.android.material.chip.ChipGroup
android:id="@+id/chipGroup"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center_vertical"
android:layout_marginEnd="96dp"
android:animateLayoutChanges="true"
app:singleLine="true">
<com.google.android.material.chip.Chip
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:visibility="gone"
tools:text="Example"
tools:visibility="visible" />
</com.google.android.material.chip.ChipGroup>
</FrameLayout>
</HorizontalScrollView>
<TextView <TextView
android:id="@+id/header_action" android:id="@+id/header_action"