Add an invite button in the new conversation screen.
parent
af42d5b671
commit
85c9a9050a
|
@ -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>
|
|
@ -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>
|
|
@ -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 -->
|
||||
<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 -->
|
||||
<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.annotation.SuppressLint;
|
||||
import android.content.Context;
|
||||
import android.database.Cursor;
|
||||
import android.os.AsyncTask;
|
||||
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.View;
|
||||
import android.view.ViewGroup;
|
||||
|
@ -37,6 +31,15 @@ import android.widget.Button;
|
|||
import android.widget.TextView;
|
||||
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 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.ContactsCursorLoader;
|
||||
import org.thoughtcrime.securesms.contacts.ContactsCursorLoader.DisplayMode;
|
||||
import org.thoughtcrime.securesms.database.CursorRecyclerViewAdapter;
|
||||
import org.thoughtcrime.securesms.logging.Log;
|
||||
import org.thoughtcrime.securesms.mms.GlideApp;
|
||||
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.TextSecurePreferences;
|
||||
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.util.LinkedList;
|
||||
|
@ -64,28 +68,41 @@ import java.util.Set;
|
|||
* @author Moxie Marlinspike
|
||||
*
|
||||
*/
|
||||
public class ContactSelectionListFragment extends Fragment
|
||||
implements LoaderManager.LoaderCallbacks<Cursor>
|
||||
public final class ContactSelectionListFragment extends Fragment
|
||||
implements LoaderManager.LoaderCallbacks<Cursor>
|
||||
{
|
||||
@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 MULTI_SELECT = "multi_select";
|
||||
public static final String REFRESHABLE = "refreshable";
|
||||
public static final String RECENTS = "recents";
|
||||
|
||||
private TextView emptyText;
|
||||
private Set<String> selectedContacts;
|
||||
private OnContactSelectedListener onContactSelectedListener;
|
||||
private SwipeRefreshLayout swipeRefresh;
|
||||
private View showContactsLayout;
|
||||
private Button showContactsButton;
|
||||
private TextView showContactsDescription;
|
||||
private ProgressWheel showContactsProgress;
|
||||
private String cursorFilter;
|
||||
private RecyclerView recyclerView;
|
||||
private RecyclerViewFastScroller fastScroller;
|
||||
private TextView emptyText;
|
||||
private Set<String> selectedContacts;
|
||||
private OnContactSelectedListener onContactSelectedListener;
|
||||
private SwipeRefreshLayout swipeRefresh;
|
||||
private View showContactsLayout;
|
||||
private Button showContactsButton;
|
||||
private TextView showContactsDescription;
|
||||
private ProgressWheel showContactsProgress;
|
||||
private String cursorFilter;
|
||||
private RecyclerView recyclerView;
|
||||
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
|
||||
public void onActivityCreated(Bundle icicle) {
|
||||
|
@ -158,14 +175,31 @@ public class ContactSelectionListFragment extends Fragment
|
|||
}
|
||||
|
||||
private void initializeCursor() {
|
||||
ContactSelectionListAdapter adapter = new ContactSelectionListAdapter(getActivity(),
|
||||
GlideApp.with(this),
|
||||
null,
|
||||
new ListClickListener(),
|
||||
isMulti());
|
||||
selectedContacts = adapter.getSelectedContacts();
|
||||
recyclerView.setAdapter(adapter);
|
||||
recyclerView.addItemDecoration(new StickyHeaderDecoration(adapter, true, true));
|
||||
cursorRecyclerViewAdapter = new ContactSelectionListAdapter(requireContext(),
|
||||
GlideApp.with(this),
|
||||
null,
|
||||
new ListClickListener(),
|
||||
isMulti());
|
||||
selectedContacts = cursorRecyclerViewAdapter.getSelectedContacts();
|
||||
|
||||
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() {
|
||||
|
@ -192,7 +226,7 @@ public class ContactSelectionListFragment extends Fragment
|
|||
|
||||
public void setQueryFilter(String filter) {
|
||||
this.cursorFilter = filter;
|
||||
this.getLoaderManager().restartLoader(0, null, this);
|
||||
LoaderManager.getInstance(this).restartLoader(0, null, this);
|
||||
}
|
||||
|
||||
public void resetQueryFilter() {
|
||||
|
@ -224,9 +258,14 @@ public class ContactSelectionListFragment extends Fragment
|
|||
swipeRefresh.setVisibility(View.VISIBLE);
|
||||
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);
|
||||
boolean useFastScroller = (recyclerView.getAdapter().getItemCount() > 20);
|
||||
boolean useFastScroller = data.getCount() > 20;
|
||||
recyclerView.setVerticalScrollBarEnabled(!useFastScroller);
|
||||
if (useFastScroller) {
|
||||
fastScroller.setVisibility(View.VISIBLE);
|
||||
|
@ -239,7 +278,7 @@ public class ContactSelectionListFragment extends Fragment
|
|||
|
||||
@Override
|
||||
public void onLoaderReset(@NonNull Loader<Cursor> loader) {
|
||||
((CursorRecyclerViewAdapter) recyclerView.getAdapter()).changeCursor(null);
|
||||
cursorRecyclerViewAdapter.changeCursor(null);
|
||||
fastScroller.setVisibility(View.GONE);
|
||||
}
|
||||
|
||||
|
@ -309,4 +348,7 @@ public class ContactSelectionListFragment extends Fragment
|
|||
void onContactDeselected(String number);
|
||||
}
|
||||
|
||||
public interface InviteCallback {
|
||||
void onInvite();
|
||||
}
|
||||
}
|
||||
|
|
|
@ -93,7 +93,7 @@ public class MediaDocumentsAdapter extends CursorRecyclerViewAdapter<ViewHolder>
|
|||
}
|
||||
|
||||
@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));
|
||||
}
|
||||
|
||||
|
|
|
@ -35,7 +35,9 @@ import org.thoughtcrime.securesms.recipients.Recipient;
|
|||
* @author Moxie Marlinspike
|
||||
*
|
||||
*/
|
||||
public class NewConversationActivity extends ContactSelectionActivity {
|
||||
public class NewConversationActivity extends ContactSelectionActivity
|
||||
implements ContactSelectionListFragment.InviteCallback
|
||||
{
|
||||
|
||||
@SuppressWarnings("unused")
|
||||
private static final String TAG = NewConversationActivity.class.getSimpleName();
|
||||
|
@ -99,4 +101,9 @@ public class NewConversationActivity extends ContactSelectionActivity {
|
|||
super.onPrepareOptionsMenu(menu);
|
||||
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.ViewHolder;
|
||||
import org.thoughtcrime.securesms.database.CursorRecyclerViewAdapter;
|
||||
import org.thoughtcrime.securesms.logging.Log;
|
||||
import org.thoughtcrime.securesms.mms.GlideRequests;
|
||||
import org.thoughtcrime.securesms.util.StickyHeaderDecoration.StickyHeaderAdapter;
|
||||
import org.thoughtcrime.securesms.util.Util;
|
||||
|
@ -53,7 +54,8 @@ public class ContactSelectionListAdapter extends CursorRecyclerViewAdapter<ViewH
|
|||
implements FastScrollAdapter,
|
||||
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_DIVIDER = 1;
|
||||
|
@ -198,7 +200,7 @@ public class ContactSelectionListAdapter extends CursorRecyclerViewAdapter<ViewH
|
|||
|
||||
|
||||
@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));
|
||||
}
|
||||
|
||||
|
|
|
@ -414,7 +414,7 @@ public class ConversationAdapter <V extends View & BindableConversationItem>
|
|||
}
|
||||
|
||||
@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));
|
||||
}
|
||||
|
||||
|
|
|
@ -94,7 +94,7 @@ class SearchListAdapter extends RecyclerView.Adapter<SearchListAdapter.Search
|
|||
}
|
||||
|
||||
@Override
|
||||
public HeaderViewHolder onCreateHeaderViewHolder(ViewGroup parent) {
|
||||
public HeaderViewHolder onCreateHeaderViewHolder(ViewGroup parent, int position) {
|
||||
return new HeaderViewHolder(LayoutInflater.from(parent.getContext())
|
||||
.inflate(R.layout.contact_selection_list_divider, parent, false));
|
||||
}
|
||||
|
|
|
@ -88,7 +88,7 @@ public class StickyHeaderDecoration extends RecyclerView.ItemDecoration {
|
|||
if (headerHolder == null) {
|
||||
|
||||
if (key != StickyHeaderAdapter.NO_HEADER_ID) {
|
||||
headerHolder = adapter.onCreateHeaderViewHolder(parent );
|
||||
headerHolder = adapter.onCreateHeaderViewHolder(parent, position);
|
||||
//noinspection unchecked
|
||||
adapter.onBindHeaderViewHolder(headerHolder, position);
|
||||
}
|
||||
|
@ -221,7 +221,7 @@ public class StickyHeaderDecoration extends RecyclerView.ItemDecoration {
|
|||
* @param position position in the adapter
|
||||
* @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.
|
||||
|
|
|
@ -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…
Reference in New Issue