mirror of
https://github.com/oxen-io/session-android.git
synced 2025-06-09 19:58:34 +00:00
Add an invite button in the new conversation screen.
This commit is contained in:
parent
af42d5b671
commit
85c9a9050a
5
res/drawable/ic_invite_24dp.xml
Normal file
5
res/drawable/ic_invite_24dp.xml
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
<vector android:height="24dp" android:tint="@color/core_grey_75"
|
||||||
|
android:viewportHeight="24.0" android:viewportWidth="24.0"
|
||||||
|
android:width="24dp" xmlns:android="http://schemas.android.com/apk/res/android">
|
||||||
|
<path android:fillColor="#FF000000" android:pathData="M20,4L4,4c-1.1,0 -1.99,0.9 -1.99,2L2,18c0,1.1 0.9,2 2,2h16c1.1,0 2,-0.9 2,-2L22,6c0,-1.1 -0.9,-2 -2,-2zM20,18L4,18L4,8l8,5 8,-5v10zM12,11L4,6h16l-8,5z"/>
|
||||||
|
</vector>
|
14
res/drawable/ic_invite_circle.xml
Normal file
14
res/drawable/ic_invite_circle.xml
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
|
||||||
|
<item>
|
||||||
|
<shape android:shape="oval">
|
||||||
|
<solid android:color="#eeefef" />
|
||||||
|
</shape>
|
||||||
|
</item>
|
||||||
|
<item
|
||||||
|
android:bottom="12dp"
|
||||||
|
android:drawable="@drawable/ic_invite_24dp"
|
||||||
|
android:left="12dp"
|
||||||
|
android:right="12dp"
|
||||||
|
android:top="12dp" />
|
||||||
|
</layer-list>
|
17
res/layout/contact_selection_invite_action_item.xml
Normal file
17
res/layout/contact_selection_invite_action_item.xml
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<TextView xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
xmlns:tools="http://schemas.android.com/tools"
|
||||||
|
style="@style/Signal.Text.Body"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="@dimen/contact_selection_item_height"
|
||||||
|
android:drawableStart="@drawable/ic_invite_circle"
|
||||||
|
android:drawablePadding="16dp"
|
||||||
|
android:ellipsize="marquee"
|
||||||
|
android:fontFamily="sans-serif-medium"
|
||||||
|
android:gravity="center_vertical|start"
|
||||||
|
android:labelFor="@id/action_icon"
|
||||||
|
android:paddingStart="@dimen/selection_item_header_width"
|
||||||
|
android:singleLine="true"
|
||||||
|
android:text="@string/contact_selection_activity__invite_to_signal"
|
||||||
|
android:textAlignment="viewStart"
|
||||||
|
tools:ignore="RtlSymmetry" />
|
@ -933,6 +933,7 @@
|
|||||||
|
|
||||||
<!-- contact_selection_activity -->
|
<!-- contact_selection_activity -->
|
||||||
<string name="contact_selection_activity__enter_name_or_number">Enter name or number</string>
|
<string name="contact_selection_activity__enter_name_or_number">Enter name or number</string>
|
||||||
|
<string name="contact_selection_activity__invite_to_signal">Invite to Signal</string>
|
||||||
|
|
||||||
<!-- contact_filter_toolbar -->
|
<!-- contact_filter_toolbar -->
|
||||||
<string name="contact_filter_toolbar__clear_entered_text_description">Clear entered text</string>
|
<string name="contact_filter_toolbar__clear_entered_text_description">Clear entered text</string>
|
||||||
|
@ -19,16 +19,10 @@ package org.thoughtcrime.securesms;
|
|||||||
|
|
||||||
import android.Manifest;
|
import android.Manifest;
|
||||||
import android.annotation.SuppressLint;
|
import android.annotation.SuppressLint;
|
||||||
|
import android.content.Context;
|
||||||
import android.database.Cursor;
|
import android.database.Cursor;
|
||||||
import android.os.AsyncTask;
|
import android.os.AsyncTask;
|
||||||
import android.os.Bundle;
|
import android.os.Bundle;
|
||||||
import androidx.annotation.NonNull;
|
|
||||||
import androidx.fragment.app.Fragment;
|
|
||||||
import androidx.loader.app.LoaderManager;
|
|
||||||
import androidx.loader.content.Loader;
|
|
||||||
import androidx.swiperefreshlayout.widget.SwipeRefreshLayout;
|
|
||||||
import androidx.recyclerview.widget.LinearLayoutManager;
|
|
||||||
import androidx.recyclerview.widget.RecyclerView;
|
|
||||||
import android.view.LayoutInflater;
|
import android.view.LayoutInflater;
|
||||||
import android.view.View;
|
import android.view.View;
|
||||||
import android.view.ViewGroup;
|
import android.view.ViewGroup;
|
||||||
@ -37,6 +31,15 @@ import android.widget.Button;
|
|||||||
import android.widget.TextView;
|
import android.widget.TextView;
|
||||||
import android.widget.Toast;
|
import android.widget.Toast;
|
||||||
|
|
||||||
|
import androidx.annotation.NonNull;
|
||||||
|
import androidx.annotation.Nullable;
|
||||||
|
import androidx.fragment.app.Fragment;
|
||||||
|
import androidx.loader.app.LoaderManager;
|
||||||
|
import androidx.loader.content.Loader;
|
||||||
|
import androidx.recyclerview.widget.LinearLayoutManager;
|
||||||
|
import androidx.recyclerview.widget.RecyclerView;
|
||||||
|
import androidx.swiperefreshlayout.widget.SwipeRefreshLayout;
|
||||||
|
|
||||||
import com.pnikosis.materialishprogress.ProgressWheel;
|
import com.pnikosis.materialishprogress.ProgressWheel;
|
||||||
|
|
||||||
import org.thoughtcrime.securesms.components.RecyclerViewFastScroller;
|
import org.thoughtcrime.securesms.components.RecyclerViewFastScroller;
|
||||||
@ -44,7 +47,6 @@ 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;
|
||||||
import org.thoughtcrime.securesms.contacts.ContactsCursorLoader.DisplayMode;
|
import org.thoughtcrime.securesms.contacts.ContactsCursorLoader.DisplayMode;
|
||||||
import org.thoughtcrime.securesms.database.CursorRecyclerViewAdapter;
|
|
||||||
import org.thoughtcrime.securesms.logging.Log;
|
import org.thoughtcrime.securesms.logging.Log;
|
||||||
import org.thoughtcrime.securesms.mms.GlideApp;
|
import org.thoughtcrime.securesms.mms.GlideApp;
|
||||||
import org.thoughtcrime.securesms.permissions.Permissions;
|
import org.thoughtcrime.securesms.permissions.Permissions;
|
||||||
@ -52,6 +54,8 @@ import org.thoughtcrime.securesms.util.DirectoryHelper;
|
|||||||
import org.thoughtcrime.securesms.util.StickyHeaderDecoration;
|
import org.thoughtcrime.securesms.util.StickyHeaderDecoration;
|
||||||
import org.thoughtcrime.securesms.util.TextSecurePreferences;
|
import org.thoughtcrime.securesms.util.TextSecurePreferences;
|
||||||
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.RecyclerViewConcatenateAdapterStickyHeader;
|
||||||
|
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.util.LinkedList;
|
import java.util.LinkedList;
|
||||||
@ -64,28 +68,41 @@ import java.util.Set;
|
|||||||
* @author Moxie Marlinspike
|
* @author Moxie Marlinspike
|
||||||
*
|
*
|
||||||
*/
|
*/
|
||||||
public class ContactSelectionListFragment extends Fragment
|
public final class ContactSelectionListFragment extends Fragment
|
||||||
implements LoaderManager.LoaderCallbacks<Cursor>
|
implements LoaderManager.LoaderCallbacks<Cursor>
|
||||||
{
|
{
|
||||||
@SuppressWarnings("unused")
|
@SuppressWarnings("unused")
|
||||||
private static final String TAG = ContactSelectionListFragment.class.getSimpleName();
|
private static final String TAG = Log.tag(ContactSelectionListFragment.class);
|
||||||
|
|
||||||
public static final String DISPLAY_MODE = "display_mode";
|
public static final String DISPLAY_MODE = "display_mode";
|
||||||
public static final String MULTI_SELECT = "multi_select";
|
public static final String MULTI_SELECT = "multi_select";
|
||||||
public static final String REFRESHABLE = "refreshable";
|
public static final String REFRESHABLE = "refreshable";
|
||||||
public static final String RECENTS = "recents";
|
public static final String RECENTS = "recents";
|
||||||
|
|
||||||
private TextView emptyText;
|
private TextView emptyText;
|
||||||
private Set<String> selectedContacts;
|
private Set<String> selectedContacts;
|
||||||
private OnContactSelectedListener onContactSelectedListener;
|
private OnContactSelectedListener onContactSelectedListener;
|
||||||
private SwipeRefreshLayout swipeRefresh;
|
private SwipeRefreshLayout swipeRefresh;
|
||||||
private View showContactsLayout;
|
private View showContactsLayout;
|
||||||
private Button showContactsButton;
|
private Button showContactsButton;
|
||||||
private TextView showContactsDescription;
|
private TextView showContactsDescription;
|
||||||
private ProgressWheel showContactsProgress;
|
private ProgressWheel showContactsProgress;
|
||||||
private String cursorFilter;
|
private String cursorFilter;
|
||||||
private RecyclerView recyclerView;
|
private RecyclerView recyclerView;
|
||||||
private RecyclerViewFastScroller fastScroller;
|
private RecyclerViewFastScroller fastScroller;
|
||||||
|
private ContactSelectionListAdapter cursorRecyclerViewAdapter;
|
||||||
|
|
||||||
|
@Nullable private FixedViewsAdapter footerAdapter;
|
||||||
|
@Nullable private InviteCallback inviteCallback;
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onAttach(@NonNull Context context) {
|
||||||
|
super.onAttach(context);
|
||||||
|
|
||||||
|
if (context instanceof InviteCallback) {
|
||||||
|
inviteCallback = (InviteCallback) context;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onActivityCreated(Bundle icicle) {
|
public void onActivityCreated(Bundle icicle) {
|
||||||
@ -158,14 +175,31 @@ public class ContactSelectionListFragment extends Fragment
|
|||||||
}
|
}
|
||||||
|
|
||||||
private void initializeCursor() {
|
private void initializeCursor() {
|
||||||
ContactSelectionListAdapter adapter = new ContactSelectionListAdapter(getActivity(),
|
cursorRecyclerViewAdapter = new ContactSelectionListAdapter(requireContext(),
|
||||||
GlideApp.with(this),
|
GlideApp.with(this),
|
||||||
null,
|
null,
|
||||||
new ListClickListener(),
|
new ListClickListener(),
|
||||||
isMulti());
|
isMulti());
|
||||||
selectedContacts = adapter.getSelectedContacts();
|
selectedContacts = cursorRecyclerViewAdapter.getSelectedContacts();
|
||||||
recyclerView.setAdapter(adapter);
|
|
||||||
recyclerView.addItemDecoration(new StickyHeaderDecoration(adapter, true, true));
|
RecyclerViewConcatenateAdapterStickyHeader concatenateAdapter = new RecyclerViewConcatenateAdapterStickyHeader();
|
||||||
|
|
||||||
|
concatenateAdapter.addAdapter(cursorRecyclerViewAdapter);
|
||||||
|
if (inviteCallback != null) {
|
||||||
|
footerAdapter = new FixedViewsAdapter(createInviteActionView(inviteCallback));
|
||||||
|
footerAdapter.hide();
|
||||||
|
concatenateAdapter.addAdapter(footerAdapter);
|
||||||
|
}
|
||||||
|
|
||||||
|
recyclerView.setAdapter(concatenateAdapter);
|
||||||
|
recyclerView.addItemDecoration(new StickyHeaderDecoration(concatenateAdapter, true, true));
|
||||||
|
}
|
||||||
|
|
||||||
|
private View createInviteActionView(@NonNull InviteCallback inviteCallback) {
|
||||||
|
View view = LayoutInflater.from(requireContext())
|
||||||
|
.inflate(R.layout.contact_selection_invite_action_item, (ViewGroup) requireView(), false);
|
||||||
|
view.setOnClickListener(v -> inviteCallback.onInvite());
|
||||||
|
return view;
|
||||||
}
|
}
|
||||||
|
|
||||||
private void initializeNoContactsPermission() {
|
private void initializeNoContactsPermission() {
|
||||||
@ -192,7 +226,7 @@ public class ContactSelectionListFragment extends Fragment
|
|||||||
|
|
||||||
public void setQueryFilter(String filter) {
|
public void setQueryFilter(String filter) {
|
||||||
this.cursorFilter = filter;
|
this.cursorFilter = filter;
|
||||||
this.getLoaderManager().restartLoader(0, null, this);
|
LoaderManager.getInstance(this).restartLoader(0, null, this);
|
||||||
}
|
}
|
||||||
|
|
||||||
public void resetQueryFilter() {
|
public void resetQueryFilter() {
|
||||||
@ -224,9 +258,14 @@ public class ContactSelectionListFragment extends Fragment
|
|||||||
swipeRefresh.setVisibility(View.VISIBLE);
|
swipeRefresh.setVisibility(View.VISIBLE);
|
||||||
showContactsLayout.setVisibility(View.GONE);
|
showContactsLayout.setVisibility(View.GONE);
|
||||||
|
|
||||||
((CursorRecyclerViewAdapter) recyclerView.getAdapter()).changeCursor(data);
|
cursorRecyclerViewAdapter.changeCursor(data);
|
||||||
|
|
||||||
|
if (footerAdapter != null) {
|
||||||
|
footerAdapter.show();
|
||||||
|
}
|
||||||
|
|
||||||
emptyText.setText(R.string.contact_selection_group_activity__no_contacts);
|
emptyText.setText(R.string.contact_selection_group_activity__no_contacts);
|
||||||
boolean useFastScroller = (recyclerView.getAdapter().getItemCount() > 20);
|
boolean useFastScroller = data.getCount() > 20;
|
||||||
recyclerView.setVerticalScrollBarEnabled(!useFastScroller);
|
recyclerView.setVerticalScrollBarEnabled(!useFastScroller);
|
||||||
if (useFastScroller) {
|
if (useFastScroller) {
|
||||||
fastScroller.setVisibility(View.VISIBLE);
|
fastScroller.setVisibility(View.VISIBLE);
|
||||||
@ -239,7 +278,7 @@ public class ContactSelectionListFragment extends Fragment
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onLoaderReset(@NonNull Loader<Cursor> loader) {
|
public void onLoaderReset(@NonNull Loader<Cursor> loader) {
|
||||||
((CursorRecyclerViewAdapter) recyclerView.getAdapter()).changeCursor(null);
|
cursorRecyclerViewAdapter.changeCursor(null);
|
||||||
fastScroller.setVisibility(View.GONE);
|
fastScroller.setVisibility(View.GONE);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -309,4 +348,7 @@ public class ContactSelectionListFragment extends Fragment
|
|||||||
void onContactDeselected(String number);
|
void onContactDeselected(String number);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public interface InviteCallback {
|
||||||
|
void onInvite();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -93,7 +93,7 @@ public class MediaDocumentsAdapter extends CursorRecyclerViewAdapter<ViewHolder>
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public HeaderViewHolder onCreateHeaderViewHolder(ViewGroup parent) {
|
public HeaderViewHolder onCreateHeaderViewHolder(ViewGroup parent, int position) {
|
||||||
return new HeaderViewHolder(LayoutInflater.from(getContext()).inflate(R.layout.media_overview_document_item_header, parent, false));
|
return new HeaderViewHolder(LayoutInflater.from(getContext()).inflate(R.layout.media_overview_document_item_header, parent, false));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -35,7 +35,9 @@ import org.thoughtcrime.securesms.recipients.Recipient;
|
|||||||
* @author Moxie Marlinspike
|
* @author Moxie Marlinspike
|
||||||
*
|
*
|
||||||
*/
|
*/
|
||||||
public class NewConversationActivity extends ContactSelectionActivity {
|
public class NewConversationActivity extends ContactSelectionActivity
|
||||||
|
implements ContactSelectionListFragment.InviteCallback
|
||||||
|
{
|
||||||
|
|
||||||
@SuppressWarnings("unused")
|
@SuppressWarnings("unused")
|
||||||
private static final String TAG = NewConversationActivity.class.getSimpleName();
|
private static final String TAG = NewConversationActivity.class.getSimpleName();
|
||||||
@ -99,4 +101,9 @@ public class NewConversationActivity extends ContactSelectionActivity {
|
|||||||
super.onPrepareOptionsMenu(menu);
|
super.onPrepareOptionsMenu(menu);
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onInvite() {
|
||||||
|
handleInvite();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -37,6 +37,7 @@ import org.thoughtcrime.securesms.components.RecyclerViewFastScroller.FastScroll
|
|||||||
import org.thoughtcrime.securesms.contacts.ContactSelectionListAdapter.HeaderViewHolder;
|
import org.thoughtcrime.securesms.contacts.ContactSelectionListAdapter.HeaderViewHolder;
|
||||||
import org.thoughtcrime.securesms.contacts.ContactSelectionListAdapter.ViewHolder;
|
import org.thoughtcrime.securesms.contacts.ContactSelectionListAdapter.ViewHolder;
|
||||||
import org.thoughtcrime.securesms.database.CursorRecyclerViewAdapter;
|
import org.thoughtcrime.securesms.database.CursorRecyclerViewAdapter;
|
||||||
|
import org.thoughtcrime.securesms.logging.Log;
|
||||||
import org.thoughtcrime.securesms.mms.GlideRequests;
|
import org.thoughtcrime.securesms.mms.GlideRequests;
|
||||||
import org.thoughtcrime.securesms.util.StickyHeaderDecoration.StickyHeaderAdapter;
|
import org.thoughtcrime.securesms.util.StickyHeaderDecoration.StickyHeaderAdapter;
|
||||||
import org.thoughtcrime.securesms.util.Util;
|
import org.thoughtcrime.securesms.util.Util;
|
||||||
@ -53,7 +54,8 @@ public class ContactSelectionListAdapter extends CursorRecyclerViewAdapter<ViewH
|
|||||||
implements FastScrollAdapter,
|
implements FastScrollAdapter,
|
||||||
StickyHeaderAdapter<HeaderViewHolder>
|
StickyHeaderAdapter<HeaderViewHolder>
|
||||||
{
|
{
|
||||||
private final static String TAG = ContactSelectionListAdapter.class.getSimpleName();
|
@SuppressWarnings("unused")
|
||||||
|
private final static String TAG = Log.tag(ContactSelectionListAdapter.class);
|
||||||
|
|
||||||
private static final int VIEW_TYPE_CONTACT = 0;
|
private static final int VIEW_TYPE_CONTACT = 0;
|
||||||
private static final int VIEW_TYPE_DIVIDER = 1;
|
private static final int VIEW_TYPE_DIVIDER = 1;
|
||||||
@ -198,7 +200,7 @@ public class ContactSelectionListAdapter extends CursorRecyclerViewAdapter<ViewH
|
|||||||
|
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public HeaderViewHolder onCreateHeaderViewHolder(ViewGroup parent) {
|
public HeaderViewHolder onCreateHeaderViewHolder(ViewGroup parent, int position) {
|
||||||
return new HeaderViewHolder(LayoutInflater.from(getContext()).inflate(R.layout.contact_selection_recyclerview_header, parent, false));
|
return new HeaderViewHolder(LayoutInflater.from(getContext()).inflate(R.layout.contact_selection_recyclerview_header, parent, false));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -414,7 +414,7 @@ public class ConversationAdapter <V extends View & BindableConversationItem>
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public HeaderViewHolder onCreateHeaderViewHolder(ViewGroup parent) {
|
public HeaderViewHolder onCreateHeaderViewHolder(ViewGroup parent, int position) {
|
||||||
return new HeaderViewHolder(LayoutInflater.from(getContext()).inflate(R.layout.conversation_item_header, parent, false));
|
return new HeaderViewHolder(LayoutInflater.from(getContext()).inflate(R.layout.conversation_item_header, parent, false));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -94,7 +94,7 @@ class SearchListAdapter extends RecyclerView.Adapter<SearchListAdapter.Search
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public HeaderViewHolder onCreateHeaderViewHolder(ViewGroup parent) {
|
public HeaderViewHolder onCreateHeaderViewHolder(ViewGroup parent, int position) {
|
||||||
return new HeaderViewHolder(LayoutInflater.from(parent.getContext())
|
return new HeaderViewHolder(LayoutInflater.from(parent.getContext())
|
||||||
.inflate(R.layout.contact_selection_list_divider, parent, false));
|
.inflate(R.layout.contact_selection_list_divider, parent, false));
|
||||||
}
|
}
|
||||||
|
@ -88,7 +88,7 @@ public class StickyHeaderDecoration extends RecyclerView.ItemDecoration {
|
|||||||
if (headerHolder == null) {
|
if (headerHolder == null) {
|
||||||
|
|
||||||
if (key != StickyHeaderAdapter.NO_HEADER_ID) {
|
if (key != StickyHeaderAdapter.NO_HEADER_ID) {
|
||||||
headerHolder = adapter.onCreateHeaderViewHolder(parent );
|
headerHolder = adapter.onCreateHeaderViewHolder(parent, position);
|
||||||
//noinspection unchecked
|
//noinspection unchecked
|
||||||
adapter.onBindHeaderViewHolder(headerHolder, position);
|
adapter.onBindHeaderViewHolder(headerHolder, position);
|
||||||
}
|
}
|
||||||
@ -221,7 +221,7 @@ public class StickyHeaderDecoration extends RecyclerView.ItemDecoration {
|
|||||||
* @param position position in the adapter
|
* @param position position in the adapter
|
||||||
* @return a view holder for the created view
|
* @return a view holder for the created view
|
||||||
*/
|
*/
|
||||||
T onCreateHeaderViewHolder(ViewGroup parent);
|
T onCreateHeaderViewHolder(ViewGroup parent, int position);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Updates the header view to reflect the header data for the given position.
|
* Updates the header view to reflect the header data for the given position.
|
||||||
|
@ -0,0 +1,67 @@
|
|||||||
|
package org.thoughtcrime.securesms.util.adapter;
|
||||||
|
|
||||||
|
import android.view.View;
|
||||||
|
import android.view.ViewGroup;
|
||||||
|
|
||||||
|
import androidx.annotation.NonNull;
|
||||||
|
import androidx.recyclerview.widget.RecyclerView;
|
||||||
|
|
||||||
|
import java.util.Arrays;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
public final class FixedViewsAdapter extends RecyclerView.Adapter<RecyclerView.ViewHolder> {
|
||||||
|
|
||||||
|
private final List<View> viewList;
|
||||||
|
|
||||||
|
private boolean hidden;
|
||||||
|
|
||||||
|
public FixedViewsAdapter(@NonNull View... viewList) {
|
||||||
|
this.viewList = Arrays.asList(viewList);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int getItemCount() {
|
||||||
|
return hidden ? 0 : viewList.size();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return View type is the index.
|
||||||
|
*/
|
||||||
|
@Override
|
||||||
|
public int getItemViewType(int position) {
|
||||||
|
return position;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param viewType The index in the list of views.
|
||||||
|
*/
|
||||||
|
@Override
|
||||||
|
public RecyclerView.ViewHolder onCreateViewHolder(@NonNull ViewGroup viewGroup, int viewType) {
|
||||||
|
return new RecyclerView.ViewHolder(viewList.get(viewType)) {
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onBindViewHolder(@NonNull RecyclerView.ViewHolder viewHolder, int position) {
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public long getItemId(int position) {
|
||||||
|
return position;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void hide() {
|
||||||
|
setHidden(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void show() {
|
||||||
|
setHidden(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void setHidden(boolean hidden) {
|
||||||
|
if (this.hidden != hidden) {
|
||||||
|
this.hidden = hidden;
|
||||||
|
notifyDataSetChanged();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,292 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (C) 2017 Martijn van der Woude
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*
|
||||||
|
* Original source: https://github.com/martijnvdwoude/recycler-view-merge-adapter
|
||||||
|
*
|
||||||
|
* This file has been modified by Signal.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package org.thoughtcrime.securesms.util.adapter;
|
||||||
|
|
||||||
|
import android.util.LongSparseArray;
|
||||||
|
import android.util.SparseIntArray;
|
||||||
|
import android.view.ViewGroup;
|
||||||
|
|
||||||
|
import androidx.annotation.NonNull;
|
||||||
|
import androidx.recyclerview.widget.RecyclerView;
|
||||||
|
|
||||||
|
import java.util.LinkedList;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
public class RecyclerViewConcatenateAdapter extends RecyclerView.Adapter<RecyclerView.ViewHolder> {
|
||||||
|
|
||||||
|
private final List<ChildAdapter> adapters = new LinkedList<>();
|
||||||
|
|
||||||
|
private long nextUnassignedItemId;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Map of global view type to local adapter.
|
||||||
|
* <p>
|
||||||
|
* Not the same as {@link #adapters}, it may have duplicates and may be in a different order.
|
||||||
|
*/
|
||||||
|
private final List<ChildAdapter> viewTypes = new LinkedList<>();
|
||||||
|
|
||||||
|
/** Observes a single sub adapter and maps the positions on the events to global positions. */
|
||||||
|
private static class AdapterDataObserver extends RecyclerView.AdapterDataObserver {
|
||||||
|
|
||||||
|
private final RecyclerViewConcatenateAdapter mergeAdapter;
|
||||||
|
private final RecyclerView.Adapter<RecyclerView.ViewHolder> adapter;
|
||||||
|
|
||||||
|
AdapterDataObserver(RecyclerViewConcatenateAdapter mergeAdapter, RecyclerView.Adapter<RecyclerView.ViewHolder> adapter) {
|
||||||
|
this.mergeAdapter = mergeAdapter;
|
||||||
|
this.adapter = adapter;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onChanged() {
|
||||||
|
mergeAdapter.notifyDataSetChanged();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onItemRangeChanged(int positionStart, int itemCount) {
|
||||||
|
int subAdapterOffset = mergeAdapter.getSubAdapterFirstGlobalPosition(adapter);
|
||||||
|
|
||||||
|
mergeAdapter.notifyItemRangeChanged(subAdapterOffset + positionStart, itemCount);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onItemRangeInserted(int positionStart, int itemCount) {
|
||||||
|
int subAdapterOffset = mergeAdapter.getSubAdapterFirstGlobalPosition(adapter);
|
||||||
|
|
||||||
|
mergeAdapter.notifyItemRangeInserted(subAdapterOffset + positionStart, itemCount);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onItemRangeRemoved(int positionStart, int itemCount) {
|
||||||
|
int subAdapterOffset = mergeAdapter.getSubAdapterFirstGlobalPosition(adapter);
|
||||||
|
|
||||||
|
mergeAdapter.notifyItemRangeRemoved(subAdapterOffset + positionStart, itemCount);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static class ChildAdapter {
|
||||||
|
|
||||||
|
final RecyclerView.Adapter<RecyclerView.ViewHolder> adapter;
|
||||||
|
|
||||||
|
/** Map of global view types to local view types */
|
||||||
|
private final SparseIntArray globalViewTypesMap = new SparseIntArray();
|
||||||
|
|
||||||
|
/** Map of local view types to global view types */
|
||||||
|
private final SparseIntArray localViewTypesMap = new SparseIntArray();
|
||||||
|
|
||||||
|
private final AdapterDataObserver adapterDataObserver;
|
||||||
|
|
||||||
|
/** Map of local ids to global ids. */
|
||||||
|
private final LongSparseArray<Long> localItemIdMap = new LongSparseArray<>();
|
||||||
|
|
||||||
|
ChildAdapter(@NonNull RecyclerView.Adapter<RecyclerView.ViewHolder> adapter, @NonNull AdapterDataObserver adapterDataObserver) {
|
||||||
|
this.adapter = adapter;
|
||||||
|
this.adapterDataObserver = adapterDataObserver;
|
||||||
|
|
||||||
|
this.adapter.registerAdapterDataObserver(this.adapterDataObserver);
|
||||||
|
}
|
||||||
|
|
||||||
|
int getGlobalItemViewType(int localPosition, int defaultValue) {
|
||||||
|
int localViewType = adapter.getItemViewType(localPosition);
|
||||||
|
int globalViewType = localViewTypesMap.get(localViewType, defaultValue);
|
||||||
|
|
||||||
|
if (globalViewType == defaultValue) {
|
||||||
|
globalViewTypesMap.append(globalViewType, localViewType);
|
||||||
|
localViewTypesMap.append(localViewType, globalViewType);
|
||||||
|
}
|
||||||
|
|
||||||
|
return globalViewType;
|
||||||
|
}
|
||||||
|
|
||||||
|
long getGlobalItemId(int localPosition, long defaultGlobalValue) {
|
||||||
|
final long localItemId = adapter.getItemId(localPosition);
|
||||||
|
|
||||||
|
if (RecyclerView.NO_ID == localItemId) {
|
||||||
|
return RecyclerView.NO_ID;
|
||||||
|
}
|
||||||
|
|
||||||
|
final Long globalItemId = localItemIdMap.get(localItemId);
|
||||||
|
|
||||||
|
if (globalItemId == null) {
|
||||||
|
localItemIdMap.put(localItemId, defaultGlobalValue);
|
||||||
|
return defaultGlobalValue;
|
||||||
|
}
|
||||||
|
|
||||||
|
return globalItemId;
|
||||||
|
}
|
||||||
|
|
||||||
|
void unregister() {
|
||||||
|
adapter.unregisterAdapterDataObserver(adapterDataObserver);
|
||||||
|
}
|
||||||
|
|
||||||
|
RecyclerView.ViewHolder onCreateViewHolder(@NonNull ViewGroup viewGroup, int globalViewType) {
|
||||||
|
int localViewType = globalViewTypesMap.get(globalViewType);
|
||||||
|
|
||||||
|
return adapter.onCreateViewHolder(viewGroup, localViewType);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static class ChildAdapterPositionPair {
|
||||||
|
|
||||||
|
final ChildAdapter childAdapter;
|
||||||
|
final int localPosition;
|
||||||
|
|
||||||
|
ChildAdapterPositionPair(@NonNull ChildAdapter adapter, int position) {
|
||||||
|
childAdapter = adapter;
|
||||||
|
localPosition = position;
|
||||||
|
}
|
||||||
|
|
||||||
|
RecyclerView.Adapter<RecyclerView.ViewHolder> getAdapter() {
|
||||||
|
return childAdapter.adapter;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param adapter Append an adapter to the list of adapters.
|
||||||
|
*/
|
||||||
|
public void addAdapter(@NonNull RecyclerView.Adapter<RecyclerView.ViewHolder> adapter) {
|
||||||
|
addAdapter(adapters.size(), adapter);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param index The index at which to add an adapter to the list of adapters.
|
||||||
|
* @param adapter The adapter to add.
|
||||||
|
*/
|
||||||
|
public void addAdapter(int index, @NonNull RecyclerView.Adapter<RecyclerView.ViewHolder> adapter) {
|
||||||
|
AdapterDataObserver adapterDataObserver = new AdapterDataObserver(this, adapter);
|
||||||
|
adapters.add(index, new ChildAdapter(adapter, adapterDataObserver));
|
||||||
|
notifyDataSetChanged();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Clear all adapters from the list of adapters.
|
||||||
|
*/
|
||||||
|
public void clearAdapters() {
|
||||||
|
for (ChildAdapter childAdapter : adapters) {
|
||||||
|
childAdapter.unregister();
|
||||||
|
}
|
||||||
|
|
||||||
|
adapters.clear();
|
||||||
|
notifyDataSetChanged();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return a childAdapterPositionPair object for a given global position.
|
||||||
|
*
|
||||||
|
* @param globalPosition The global position in the entire set of items.
|
||||||
|
* @return A childAdapterPositionPair object containing a reference to the adapter and the local
|
||||||
|
* position in that adapter that corresponds to the given global position.
|
||||||
|
*/
|
||||||
|
@NonNull
|
||||||
|
ChildAdapterPositionPair getLocalPosition(final int globalPosition) {
|
||||||
|
int count = 0;
|
||||||
|
|
||||||
|
for (ChildAdapter childAdapter : adapters) {
|
||||||
|
int newCount = count + childAdapter.adapter.getItemCount();
|
||||||
|
|
||||||
|
if (globalPosition < newCount) {
|
||||||
|
return new ChildAdapterPositionPair(childAdapter, globalPosition - count);
|
||||||
|
}
|
||||||
|
|
||||||
|
count = newCount;
|
||||||
|
}
|
||||||
|
|
||||||
|
throw new AssertionError("Position out of range");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
@NonNull
|
||||||
|
public RecyclerView.ViewHolder onCreateViewHolder(@NonNull ViewGroup viewGroup, int viewType) {
|
||||||
|
ChildAdapter childAdapter = viewTypes.get(viewType);
|
||||||
|
if (childAdapter == null) {
|
||||||
|
throw new AssertionError("Unknown view type");
|
||||||
|
}
|
||||||
|
|
||||||
|
return childAdapter.onCreateViewHolder(viewGroup, viewType);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return the first global position in the entire set of items for a given adapter.
|
||||||
|
*
|
||||||
|
* @param adapter The adapter for which to the return the first global position.
|
||||||
|
* @return The first global position for the given adapter, or -1 if no such position could be found.
|
||||||
|
*/
|
||||||
|
private int getSubAdapterFirstGlobalPosition(@NonNull RecyclerView.Adapter adapter) {
|
||||||
|
int count = 0;
|
||||||
|
|
||||||
|
for (ChildAdapter childAdapterWrapper : adapters) {
|
||||||
|
RecyclerView.Adapter childAdapter = childAdapterWrapper.adapter;
|
||||||
|
|
||||||
|
if (childAdapter == adapter) {
|
||||||
|
return count;
|
||||||
|
}
|
||||||
|
|
||||||
|
count += childAdapter.getItemCount();
|
||||||
|
}
|
||||||
|
|
||||||
|
throw new AssertionError("Adapter not found in list of adapters");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onBindViewHolder(@NonNull RecyclerView.ViewHolder viewHolder, int position) {
|
||||||
|
ChildAdapterPositionPair childAdapterPositionPair = getLocalPosition(position);
|
||||||
|
RecyclerView.Adapter adapter = childAdapterPositionPair.getAdapter();
|
||||||
|
//noinspection unchecked
|
||||||
|
adapter.onBindViewHolder(viewHolder, childAdapterPositionPair.localPosition);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int getItemViewType(int position) {
|
||||||
|
int nextUnassignedViewType = viewTypes.size();
|
||||||
|
ChildAdapterPositionPair localPosition = getLocalPosition(position);
|
||||||
|
|
||||||
|
int viewType = localPosition.childAdapter.getGlobalItemViewType(localPosition.localPosition, nextUnassignedViewType);
|
||||||
|
|
||||||
|
if (viewType == nextUnassignedViewType) {
|
||||||
|
viewTypes.add(viewType, localPosition.childAdapter);
|
||||||
|
}
|
||||||
|
|
||||||
|
return viewType;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public long getItemId(int position) {
|
||||||
|
ChildAdapterPositionPair localPosition = getLocalPosition(position);
|
||||||
|
|
||||||
|
long itemId = localPosition.childAdapter.getGlobalItemId(localPosition.localPosition, nextUnassignedItemId);
|
||||||
|
|
||||||
|
if (itemId == nextUnassignedItemId) {
|
||||||
|
nextUnassignedItemId++;
|
||||||
|
}
|
||||||
|
|
||||||
|
return itemId;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int getItemCount() {
|
||||||
|
int count = 0;
|
||||||
|
|
||||||
|
for (ChildAdapter adapter : adapters) {
|
||||||
|
count += adapter.adapter.getItemCount();
|
||||||
|
}
|
||||||
|
|
||||||
|
return count;
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,61 @@
|
|||||||
|
package org.thoughtcrime.securesms.util.adapter;
|
||||||
|
|
||||||
|
import android.view.ViewGroup;
|
||||||
|
|
||||||
|
import androidx.recyclerview.widget.RecyclerView;
|
||||||
|
|
||||||
|
import org.thoughtcrime.securesms.components.RecyclerViewFastScroller;
|
||||||
|
import org.thoughtcrime.securesms.util.StickyHeaderDecoration;
|
||||||
|
import org.whispersystems.libsignal.util.Pair;
|
||||||
|
import org.whispersystems.libsignal.util.guava.Optional;
|
||||||
|
|
||||||
|
public final class RecyclerViewConcatenateAdapterStickyHeader extends RecyclerViewConcatenateAdapter
|
||||||
|
implements StickyHeaderDecoration.StickyHeaderAdapter,
|
||||||
|
RecyclerViewFastScroller.FastScrollAdapter
|
||||||
|
{
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public long getHeaderId(int position) {
|
||||||
|
return getForPosition(position).transform(p -> p.first().getHeaderId(p.second())).or(-1L);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public RecyclerView.ViewHolder onCreateHeaderViewHolder(ViewGroup parent, int position) {
|
||||||
|
return getForPosition(position).transform(p -> p.first().onCreateHeaderViewHolder(parent, p.second())).orNull();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onBindHeaderViewHolder(RecyclerView.ViewHolder viewHolder, int position) {
|
||||||
|
Optional<Pair<StickyHeaderDecoration.StickyHeaderAdapter, Integer>> forPosition = getForPosition(position);
|
||||||
|
|
||||||
|
if (forPosition.isPresent()) {
|
||||||
|
Pair<StickyHeaderDecoration.StickyHeaderAdapter, Integer> stickyHeaderAdapterIntegerPair = forPosition.get();
|
||||||
|
//noinspection unchecked
|
||||||
|
stickyHeaderAdapterIntegerPair.first().onBindHeaderViewHolder(viewHolder, stickyHeaderAdapterIntegerPair.second());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public CharSequence getBubbleText(int position) {
|
||||||
|
Optional<Pair<StickyHeaderDecoration.StickyHeaderAdapter, Integer>> forPosition = getForPosition(position);
|
||||||
|
|
||||||
|
return forPosition.transform(a -> {
|
||||||
|
if (a.first() instanceof RecyclerViewFastScroller.FastScrollAdapter) {
|
||||||
|
return ((RecyclerViewFastScroller.FastScrollAdapter) a.first()).getBubbleText(a.second());
|
||||||
|
} else {
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
}).or("");
|
||||||
|
}
|
||||||
|
|
||||||
|
private Optional<Pair<StickyHeaderDecoration.StickyHeaderAdapter, Integer>> getForPosition(int position) {
|
||||||
|
ChildAdapterPositionPair localAdapterPosition = getLocalPosition(position);
|
||||||
|
RecyclerView.Adapter<? extends RecyclerView.ViewHolder> adapter = localAdapterPosition.getAdapter();
|
||||||
|
|
||||||
|
if (adapter instanceof StickyHeaderDecoration.StickyHeaderAdapter) {
|
||||||
|
StickyHeaderDecoration.StickyHeaderAdapter sticky = (StickyHeaderDecoration.StickyHeaderAdapter) adapter;
|
||||||
|
return Optional.of(new Pair<>(sticky, localAdapterPosition.localPosition));
|
||||||
|
}
|
||||||
|
return Optional.absent();
|
||||||
|
}
|
||||||
|
}
|
Loading…
x
Reference in New Issue
Block a user