First try implementing sticky section headers
This commit is contained in:
@@ -33,6 +33,7 @@ dependencies {
|
||||
compile 'org.apache.james:apache-mime4j-dom:0.7.2'
|
||||
compile 'org.thoughtcrime.ssl.pinning:AndroidPinning:1.0.0'
|
||||
compile 'com.cocosw:bottomsheet:1.3.0@aar'
|
||||
compile 'com.tonicartos:superslim:0.4.13'
|
||||
|
||||
// Material Drawer
|
||||
compile 'com.mikepenz:materialdrawer:5.2.2@aar'
|
||||
|
||||
@@ -35,6 +35,7 @@ import android.support.v4.app.LoaderManager;
|
||||
import android.support.v4.content.CursorLoader;
|
||||
import android.support.v4.content.Loader;
|
||||
import android.support.v4.view.MenuItemCompat;
|
||||
import android.support.v7.widget.RecyclerView;
|
||||
import android.support.v7.widget.SearchView;
|
||||
import android.text.TextUtils;
|
||||
import android.view.ActionMode;
|
||||
@@ -54,6 +55,8 @@ import android.widget.ViewAnimator;
|
||||
|
||||
import com.getbase.floatingactionbutton.FloatingActionButton;
|
||||
import com.getbase.floatingactionbutton.FloatingActionsMenu;
|
||||
import com.tonicartos.superslim.LayoutManager;
|
||||
|
||||
import org.sufficientlysecure.keychain.Constants;
|
||||
import org.sufficientlysecure.keychain.R;
|
||||
import org.sufficientlysecure.keychain.keyimport.ParcelableKeyRing;
|
||||
@@ -74,6 +77,7 @@ import org.sufficientlysecure.keychain.ui.util.FormattingUtils;
|
||||
import org.sufficientlysecure.keychain.ui.util.KeyFormattingUtils;
|
||||
import org.sufficientlysecure.keychain.ui.util.ContentDescriptionHint;
|
||||
import org.sufficientlysecure.keychain.ui.util.Notify;
|
||||
import org.sufficientlysecure.keychain.ui.util.adapter.KeySectionedListAdapter;
|
||||
import org.sufficientlysecure.keychain.util.FabContainer;
|
||||
import org.sufficientlysecure.keychain.util.Log;
|
||||
import org.sufficientlysecure.keychain.util.Preferences;
|
||||
@@ -97,8 +101,9 @@ public class KeyListFragment extends LoaderFragment
|
||||
private static final int REQUEST_DELETE = 2;
|
||||
private static final int REQUEST_VIEW_KEY = 3;
|
||||
|
||||
private KeyListAdapter mAdapter;
|
||||
private StickyListHeadersListView mStickyList;
|
||||
//private KeyListAdapter mAdapter;
|
||||
private KeySectionedListAdapter mAdapter;
|
||||
private RecyclerView mStickyList;
|
||||
|
||||
// saves the mode object for multiselect, needed for reset at some point
|
||||
private ActionMode mActionMode = null;
|
||||
@@ -125,8 +130,8 @@ public class KeyListFragment extends LoaderFragment
|
||||
View root = super.onCreateView(inflater, superContainer, savedInstanceState);
|
||||
View view = inflater.inflate(R.layout.key_list_fragment, getContainer());
|
||||
|
||||
mStickyList = (StickyListHeadersListView) view.findViewById(R.id.key_list_list);
|
||||
mStickyList.setOnItemClickListener(this);
|
||||
mStickyList = (RecyclerView) view.findViewById(R.id.key_list_list);
|
||||
//mStickyList.setOnItemClickListener(this);
|
||||
|
||||
mFab = (FloatingActionsMenu) view.findViewById(R.id.fab_main);
|
||||
|
||||
@@ -170,13 +175,12 @@ public class KeyListFragment extends LoaderFragment
|
||||
|
||||
// show app name instead of "keys" from nav drawer
|
||||
final FragmentActivity activity = getActivity();
|
||||
|
||||
activity.setTitle(R.string.app_name);
|
||||
|
||||
mStickyList.setOnItemClickListener(this);
|
||||
mStickyList.setAreHeadersSticky(true);
|
||||
mStickyList.setDrawingListUnderStickyHeader(false);
|
||||
mStickyList.setFastScrollEnabled(true);
|
||||
//mStickyList.setOnItemClickListener(this);
|
||||
//mStickyList.setAreHeadersSticky(true);
|
||||
//mStickyList.setDrawingListUnderStickyHeader(false);
|
||||
//mStickyList.setFastScrollEnabled(true);
|
||||
|
||||
// Adds an empty footer view so that the Floating Action Button won't block content
|
||||
// in last few rows.
|
||||
@@ -192,14 +196,16 @@ public class KeyListFragment extends LoaderFragment
|
||||
);
|
||||
|
||||
footer.setLayoutParams(params);
|
||||
mStickyList.addFooterView(footer, null, false);
|
||||
//mStickyList.addFooterView(footer, null, false);
|
||||
|
||||
/*
|
||||
* Multi-selection
|
||||
*/
|
||||
mStickyList.setFastScrollAlwaysVisible(true);
|
||||
//mStickyList.setFastScrollAlwaysVisible(true);
|
||||
|
||||
mStickyList.setChoiceMode(ListView.CHOICE_MODE_MULTIPLE_MODAL);
|
||||
//mStickyList.setChoiceMode(ListView.CHOICE_MODE_MULTIPLE_MODAL);
|
||||
|
||||
/*
|
||||
mStickyList.getWrappedList().setMultiChoiceModeListener(new MultiChoiceModeListener() {
|
||||
|
||||
@Override
|
||||
@@ -257,6 +263,7 @@ public class KeyListFragment extends LoaderFragment
|
||||
}
|
||||
|
||||
});
|
||||
*/
|
||||
|
||||
// We have a menu item to show in action bar.
|
||||
setHasOptionsMenu(true);
|
||||
@@ -265,7 +272,7 @@ public class KeyListFragment extends LoaderFragment
|
||||
setContentShown(false);
|
||||
|
||||
// this view is made visible if no data is available
|
||||
mStickyList.setEmptyView(activity.findViewById(R.id.key_list_empty));
|
||||
// mStickyList.setEmptyView(activity.findViewById(R.id.key_list_empty));
|
||||
|
||||
// click on search button (in empty view) starts query for search string
|
||||
vSearchContainer = (ViewAnimator) activity.findViewById(R.id.search_container);
|
||||
@@ -278,8 +285,11 @@ public class KeyListFragment extends LoaderFragment
|
||||
});
|
||||
|
||||
// Create an empty adapter we will use to display the loaded data.
|
||||
mAdapter = new KeyListAdapter(activity, null, 0);
|
||||
//mAdapter = new KeyListAdapter(activity, null, 0);
|
||||
mAdapter = new KeySectionedListAdapter(getContext(), null);
|
||||
|
||||
mStickyList.setAdapter(mAdapter);
|
||||
mStickyList.setLayoutManager(new LayoutManager(getActivity()));
|
||||
|
||||
// Prepare the loader. Either re-connect with an existing one,
|
||||
// or start a new one.
|
||||
|
||||
@@ -0,0 +1,285 @@
|
||||
package org.sufficientlysecure.keychain.ui.util.adapter;
|
||||
|
||||
import android.content.Context;
|
||||
import android.database.ContentObserver;
|
||||
import android.database.Cursor;
|
||||
import android.database.DataSetObserver;
|
||||
import android.os.Handler;
|
||||
import android.support.v7.widget.RecyclerView;
|
||||
|
||||
import org.sufficientlysecure.keychain.util.Log;
|
||||
|
||||
public abstract class CursorAdapter extends RecyclerView.Adapter {
|
||||
|
||||
public static final String TAG = "CursorAdapter";
|
||||
|
||||
private Cursor mCursor;
|
||||
private Context mContext;
|
||||
private boolean mDataValid;
|
||||
|
||||
private ChangeObserver mChangeObserver;
|
||||
private DataSetObserver mDataSetObserver;
|
||||
|
||||
/**
|
||||
* If set the adapter will register a content observer on the cursor and will call
|
||||
* {@link #onContentChanged()} when a notification comes in. Be careful when
|
||||
* using this flag: you will need to unset the current Cursor from the adapter
|
||||
* to avoid leaks due to its registered observers. This flag is not needed
|
||||
* when using a CursorAdapter with a
|
||||
* {@link android.content.CursorLoader}.
|
||||
*/
|
||||
public static final int FLAG_REGISTER_CONTENT_OBSERVER = 0x02;
|
||||
|
||||
/**
|
||||
* Constructor that allows control over auto-requery. It is recommended
|
||||
* you not use this, but instead {@link #CursorAdapter(Context, Cursor, int)}.
|
||||
* When using this constructor, {@link #FLAG_REGISTER_CONTENT_OBSERVER}
|
||||
* will always be set.
|
||||
*
|
||||
* @param c The cursor from which to get the data.
|
||||
* @param context The context
|
||||
*/
|
||||
public CursorAdapter(Context context, Cursor c) {
|
||||
init(context, c, FLAG_REGISTER_CONTENT_OBSERVER);
|
||||
}
|
||||
|
||||
/**
|
||||
* Recommended constructor.
|
||||
*
|
||||
* @param c The cursor from which to get the data.
|
||||
* @param context The context
|
||||
* @param flags Flags used to determine the behavior of the adapter
|
||||
* @see #FLAG_REGISTER_CONTENT_OBSERVER
|
||||
*/
|
||||
public CursorAdapter(Context context, Cursor c, int flags) {
|
||||
init(context, c, flags);
|
||||
}
|
||||
|
||||
private void init(Context context, Cursor c, int flags) {
|
||||
boolean cursorPresent = c != null;
|
||||
mCursor = c;
|
||||
mDataValid = cursorPresent;
|
||||
mContext = context;
|
||||
if ((flags & FLAG_REGISTER_CONTENT_OBSERVER) == FLAG_REGISTER_CONTENT_OBSERVER) {
|
||||
mChangeObserver = new ChangeObserver();
|
||||
mDataSetObserver = new MyDataSetObserver();
|
||||
} else {
|
||||
mChangeObserver = null;
|
||||
mDataSetObserver = null;
|
||||
}
|
||||
|
||||
if (cursorPresent) {
|
||||
if (mChangeObserver != null) c.registerContentObserver(mChangeObserver);
|
||||
if (mDataSetObserver != null) c.registerDataSetObserver(mDataSetObserver);
|
||||
}
|
||||
|
||||
setHasStableIds(true);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the cursor.
|
||||
* @return the cursor.
|
||||
*/
|
||||
public Cursor getCursor() {
|
||||
return mCursor;
|
||||
}
|
||||
|
||||
public Context getContext() {
|
||||
return mContext;
|
||||
}
|
||||
|
||||
/**
|
||||
* @see android.support.v7.widget.RecyclerView.Adapter#getItemCount()
|
||||
*/
|
||||
@Override
|
||||
public int getItemCount() {
|
||||
if (mDataValid && mCursor != null) {
|
||||
return mCursor.getCount();
|
||||
} else {
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
public boolean hasValidData() {
|
||||
mDataValid = hasOpenCursor();
|
||||
return mDataValid;
|
||||
}
|
||||
|
||||
private boolean hasOpenCursor() {
|
||||
Cursor cursor = getCursor();
|
||||
if (cursor != null && cursor.isClosed()) {
|
||||
swapCursor(null);
|
||||
return false;
|
||||
}
|
||||
|
||||
return cursor != null;
|
||||
}
|
||||
|
||||
/**
|
||||
* @see android.support.v7.widget.RecyclerView.Adapter#getItemId(int)
|
||||
*
|
||||
* @param position Adapter position to query
|
||||
* @return the id of the item
|
||||
*/
|
||||
@Override
|
||||
public long getItemId(int position) {
|
||||
if (mDataValid && mCursor != null) {
|
||||
if (moveCursor(position)) {
|
||||
return getIdFromCursor(mCursor);
|
||||
} else {
|
||||
return RecyclerView.NO_ID;
|
||||
}
|
||||
} else {
|
||||
return RecyclerView.NO_ID;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the id of the item represented by the row the cursor
|
||||
* is currently moved to.
|
||||
* @param cursor The cursor moved to the correct position.
|
||||
* @return The id of the dataset
|
||||
*/
|
||||
public long getIdFromCursor(Cursor cursor) {
|
||||
if(cursor != null) {
|
||||
return cursor.getPosition();
|
||||
} else {
|
||||
return RecyclerView.NO_ID;
|
||||
}
|
||||
}
|
||||
|
||||
public void moveCursorOrThrow(int position)
|
||||
throws IndexOutOfBoundsException, IllegalStateException {
|
||||
|
||||
if(position >= getItemCount() || position < -1) {
|
||||
throw new IndexOutOfBoundsException("Position: " + position
|
||||
+ " is invalid for this data set!");
|
||||
}
|
||||
|
||||
if(!mDataValid) {
|
||||
throw new IllegalStateException("Attempt to move cursor over invalid data set!");
|
||||
}
|
||||
|
||||
if(!mCursor.moveToPosition(position)) {
|
||||
throw new IllegalStateException("Couldn't move cursor from position: "
|
||||
+ mCursor.getPosition() + " to position: " + position + "!");
|
||||
}
|
||||
}
|
||||
|
||||
public boolean moveCursor(int position) {
|
||||
if(position >= getItemCount() || position < -1) {
|
||||
Log.w(TAG, "Position: %d is invalid for this data set!");
|
||||
return false;
|
||||
}
|
||||
|
||||
if(!mDataValid) {
|
||||
Log.d(TAG, "Attempt to move cursor over invalid data set!");
|
||||
}
|
||||
|
||||
return mCursor.moveToPosition(position);
|
||||
}
|
||||
|
||||
/**
|
||||
* Change the underlying cursor to a new cursor. If there is an existing cursor it will be
|
||||
* closed.
|
||||
*
|
||||
* @param cursor The new cursor to be used
|
||||
*/
|
||||
public void changeCursor(Cursor cursor) {
|
||||
Cursor old = swapCursor(cursor);
|
||||
if (old != null) {
|
||||
old.close();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Swap in a new Cursor, returning the old Cursor. Unlike
|
||||
* {@link #changeCursor(Cursor)}, the returned old Cursor is <em>not</em>
|
||||
* closed.
|
||||
*
|
||||
* @param newCursor The new cursor to be used.
|
||||
* @return Returns the previously set Cursor, or null if there wasa not one.
|
||||
* If the given new Cursor is the same instance is the previously set
|
||||
* Cursor, null is also returned.
|
||||
*/
|
||||
public Cursor swapCursor(Cursor newCursor) {
|
||||
if (newCursor == mCursor) {
|
||||
return null;
|
||||
}
|
||||
|
||||
Cursor oldCursor = mCursor;
|
||||
if (oldCursor != null) {
|
||||
if (mChangeObserver != null) oldCursor.unregisterContentObserver(mChangeObserver);
|
||||
if (mDataSetObserver != null) oldCursor.unregisterDataSetObserver(mDataSetObserver);
|
||||
}
|
||||
|
||||
mCursor = newCursor;
|
||||
if (newCursor != null) {
|
||||
if (mChangeObserver != null) newCursor.registerContentObserver(mChangeObserver);
|
||||
if (mDataSetObserver != null) newCursor.registerDataSetObserver(mDataSetObserver);
|
||||
mDataValid = true;
|
||||
// notify the observers about the new cursor
|
||||
onContentChanged();
|
||||
} else {
|
||||
mDataValid = false;
|
||||
// notify the observers about the lack of a data set
|
||||
onContentChanged();
|
||||
}
|
||||
|
||||
return oldCursor;
|
||||
}
|
||||
|
||||
/**
|
||||
* <p>Converts the cursor into a CharSequence. Subclasses should override this
|
||||
* method to convert their results. The default implementation returns an
|
||||
* empty String for null values or the default String representation of
|
||||
* the value.</p>
|
||||
*
|
||||
* @param cursor the cursor to convert to a CharSequence
|
||||
* @return a CharSequence representing the value
|
||||
*/
|
||||
public CharSequence convertToString(Cursor cursor) {
|
||||
return cursor == null ? "" : cursor.toString();
|
||||
}
|
||||
|
||||
/**
|
||||
* Called when the {@link ContentObserver} on the cursor receives a change notification.
|
||||
* The default implementation provides the auto-requery logic, but may be overridden by
|
||||
* sub classes.
|
||||
*
|
||||
* @see ContentObserver#onChange(boolean)
|
||||
*/
|
||||
protected void onContentChanged() {
|
||||
notifyDataSetChanged();
|
||||
}
|
||||
|
||||
private class ChangeObserver extends ContentObserver {
|
||||
public ChangeObserver() {
|
||||
super(new Handler());
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean deliverSelfNotifications() {
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onChange(boolean selfChange) {
|
||||
onContentChanged();
|
||||
}
|
||||
}
|
||||
|
||||
private class MyDataSetObserver extends DataSetObserver {
|
||||
@Override
|
||||
public void onChanged() {
|
||||
mDataValid = true;
|
||||
onContentChanged();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onInvalidated() {
|
||||
mDataValid = false;
|
||||
onContentChanged();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,104 @@
|
||||
package org.sufficientlysecure.keychain.ui.util.adapter;
|
||||
|
||||
import android.database.Cursor;
|
||||
|
||||
import org.openintents.openpgp.util.OpenPgpUtils;
|
||||
import org.sufficientlysecure.keychain.pgp.CanonicalizedPublicKey;
|
||||
import org.sufficientlysecure.keychain.pgp.CanonicalizedPublicKeyRing;
|
||||
import org.sufficientlysecure.keychain.pgp.KeyRing;
|
||||
import org.sufficientlysecure.keychain.provider.KeychainContract;
|
||||
import org.sufficientlysecure.keychain.ui.util.KeyFormattingUtils;
|
||||
|
||||
import java.io.Serializable;
|
||||
import java.util.Date;
|
||||
|
||||
public interface KeyAdapter {
|
||||
// These are the rows that we will retrieve.
|
||||
String[] PROJECTION = new String[] {
|
||||
KeychainContract.KeyRings._ID,
|
||||
KeychainContract.KeyRings.MASTER_KEY_ID,
|
||||
KeychainContract.KeyRings.USER_ID,
|
||||
KeychainContract.KeyRings.IS_REVOKED,
|
||||
KeychainContract.KeyRings.IS_EXPIRED,
|
||||
KeychainContract.KeyRings.VERIFIED,
|
||||
KeychainContract.KeyRings.HAS_ANY_SECRET,
|
||||
KeychainContract.KeyRings.HAS_DUPLICATE_USER_ID,
|
||||
KeychainContract.KeyRings.FINGERPRINT,
|
||||
KeychainContract.KeyRings.CREATION,
|
||||
KeychainContract.KeyRings.HAS_ENCRYPT
|
||||
};
|
||||
|
||||
// projection indices
|
||||
int INDEX_MASTER_KEY_ID = 1;
|
||||
int INDEX_USER_ID = 2;
|
||||
int INDEX_IS_REVOKED = 3;
|
||||
int INDEX_IS_EXPIRED = 4;
|
||||
int INDEX_VERIFIED = 5;
|
||||
int INDEX_HAS_ANY_SECRET = 6;
|
||||
int INDEX_HAS_DUPLICATE_USER_ID = 7;
|
||||
int INDEX_FINGERPRINT = 8;
|
||||
int INDEX_CREATION = 9;
|
||||
int INDEX_HAS_ENCRYPT = 10;
|
||||
|
||||
// adapter functionality
|
||||
void setSearchQuery(String query);
|
||||
boolean isEnabled(Cursor cursor);
|
||||
|
||||
KeyItem getItem(int position);
|
||||
long getMasterKeyId(int position);
|
||||
boolean isSecretAvailable(int position);
|
||||
|
||||
class KeyItem implements Serializable {
|
||||
final String mUserIdFull;
|
||||
final OpenPgpUtils.UserId mUserId;
|
||||
final long mKeyId;
|
||||
final boolean mHasDuplicate;
|
||||
final boolean mHasEncrypt;
|
||||
final Date mCreation;
|
||||
final String mFingerprint;
|
||||
final boolean mIsSecret, mIsRevoked, mIsExpired, mIsVerified;
|
||||
|
||||
public KeyItem(Cursor cursor) {
|
||||
String userId = cursor.getString(INDEX_USER_ID);
|
||||
mUserId = KeyRing.splitUserId(userId);
|
||||
mUserIdFull = userId;
|
||||
mKeyId = cursor.getLong(INDEX_MASTER_KEY_ID);
|
||||
mHasDuplicate = cursor.getLong(INDEX_HAS_DUPLICATE_USER_ID) > 0;
|
||||
mHasEncrypt = cursor.getInt(INDEX_HAS_ENCRYPT) != 0;
|
||||
mCreation = new Date(cursor.getLong(INDEX_CREATION) * 1000);
|
||||
mFingerprint = KeyFormattingUtils.convertFingerprintToHex(
|
||||
cursor.getBlob(INDEX_FINGERPRINT));
|
||||
mIsSecret = cursor.getInt(INDEX_HAS_ANY_SECRET) != 0;
|
||||
mIsRevoked = cursor.getInt(INDEX_IS_REVOKED) > 0;
|
||||
mIsExpired = cursor.getInt(INDEX_IS_EXPIRED) > 0;
|
||||
mIsVerified = cursor.getInt(INDEX_VERIFIED) > 0;
|
||||
}
|
||||
|
||||
public KeyItem(CanonicalizedPublicKeyRing ring) {
|
||||
CanonicalizedPublicKey key = ring.getPublicKey();
|
||||
String userId = key.getPrimaryUserIdWithFallback();
|
||||
mUserId = KeyRing.splitUserId(userId);
|
||||
mUserIdFull = userId;
|
||||
mKeyId = ring.getMasterKeyId();
|
||||
mHasDuplicate = false;
|
||||
mHasEncrypt = key.getKeyRing().getEncryptIds().size() > 0;
|
||||
mCreation = key.getCreationTime();
|
||||
mFingerprint = KeyFormattingUtils.convertFingerprintToHex(
|
||||
ring.getFingerprint());
|
||||
mIsRevoked = key.isRevoked();
|
||||
mIsExpired = key.isExpired();
|
||||
|
||||
// these two are actually "don't know"s
|
||||
mIsSecret = false;
|
||||
mIsVerified = false;
|
||||
}
|
||||
|
||||
public String getReadableName() {
|
||||
if (mUserId.name != null) {
|
||||
return mUserId.name;
|
||||
} else {
|
||||
return mUserId.email;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,268 @@
|
||||
package org.sufficientlysecure.keychain.ui.util.adapter;
|
||||
|
||||
import android.content.Context;
|
||||
import android.database.Cursor;
|
||||
import android.graphics.PorterDuff;
|
||||
import android.support.v7.widget.RecyclerView;
|
||||
import android.text.format.DateUtils;
|
||||
import android.util.SparseBooleanArray;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.widget.ImageButton;
|
||||
import android.widget.ImageView;
|
||||
import android.widget.TextView;
|
||||
|
||||
import org.openintents.openpgp.util.OpenPgpUtils;
|
||||
import org.sufficientlysecure.keychain.R;
|
||||
import org.sufficientlysecure.keychain.ui.util.FormattingUtils;
|
||||
import org.sufficientlysecure.keychain.ui.util.Highlighter;
|
||||
import org.sufficientlysecure.keychain.ui.util.KeyFormattingUtils;
|
||||
|
||||
public class KeySectionedListAdapter extends SectionCursorAdapter<String, KeySectionedListAdapter.KeyItemViewHolder, KeySectionedListAdapter.KeyHeaderViewHolder> implements KeyAdapter {
|
||||
private String mQuery;
|
||||
private SparseBooleanArray mSelectionMap;
|
||||
|
||||
|
||||
public KeySectionedListAdapter(Context context, Cursor cursor) {
|
||||
super(context, cursor, 0);
|
||||
|
||||
mQuery = "";
|
||||
mSelectionMap = new SparseBooleanArray();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setSearchQuery(String query) {
|
||||
mQuery = query;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isEnabled(Cursor cursor) {
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public KeyItem getItem(int position) {
|
||||
Cursor cursor = getCursor();
|
||||
|
||||
if(cursor != null) {
|
||||
if(cursor.getPosition() != position) {
|
||||
moveCursor(position);
|
||||
}
|
||||
|
||||
return new KeyItem(cursor);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public long getMasterKeyId(int position) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isSecretAvailable(int position) {
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected String getSectionFromCursor(Cursor cursor) throws IllegalStateException {
|
||||
if (cursor.getInt(INDEX_HAS_ANY_SECRET) != 0) {
|
||||
return getContext().getString(R.string.my_keys);
|
||||
}
|
||||
|
||||
String userId = cursor.getString(INDEX_USER_ID);
|
||||
String headerText = getContext().getString(R.string.user_id_no_name);
|
||||
|
||||
if (userId != null && userId.length() > 0) {
|
||||
headerText = "" + userId.charAt(0);
|
||||
}
|
||||
|
||||
return headerText;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected KeyHeaderViewHolder onCreateSectionViewHolder(ViewGroup parent) {
|
||||
return new KeyHeaderViewHolder(LayoutInflater.from(parent.getContext())
|
||||
.inflate(R.layout.key_list_header, parent, false));
|
||||
}
|
||||
|
||||
@Override
|
||||
protected KeyItemViewHolder onCreateItemViewHolder(ViewGroup parent, int viewType) {
|
||||
return new KeyItemViewHolder(LayoutInflater.from(parent.getContext())
|
||||
.inflate(R.layout.key_list_item, parent, false));
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onBindSectionViewHolder(KeyHeaderViewHolder holder, int sectionIndex, String section) {
|
||||
System.out.println("SIX: " + sectionIndex);
|
||||
if(sectionIndex == 0) {
|
||||
holder.bind(section, getCursor().getCount());
|
||||
} else {
|
||||
holder.bind(section);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onBindItemViewHolder(KeyItemViewHolder holder, Cursor cursor) {
|
||||
boolean isSecret = cursor.getInt(INDEX_HAS_ANY_SECRET) != 0;
|
||||
long masterKeyId = cursor.getLong(INDEX_MASTER_KEY_ID);
|
||||
if (isSecret && masterKeyId == 0L) {
|
||||
holder.bindDummy();
|
||||
} else {
|
||||
Highlighter highlighter = new Highlighter(getContext(), mQuery);
|
||||
holder.bindKey(new KeyItem(cursor), highlighter);
|
||||
}
|
||||
}
|
||||
|
||||
static class KeyItemViewHolder extends RecyclerView.ViewHolder {
|
||||
private View mLayoutDummy;
|
||||
private View mLayoutData;
|
||||
private Long mMasterKeyId;
|
||||
private TextView mMainUserId;
|
||||
private TextView mMainUserIdRest;
|
||||
private TextView mCreationDate;
|
||||
private ImageView mStatus;
|
||||
private View mSlinger;
|
||||
private ImageButton mSlingerButton;
|
||||
|
||||
public KeyItemViewHolder(View itemView) {
|
||||
super(itemView);
|
||||
|
||||
mLayoutData = itemView.findViewById(R.id.key_list_item_data);
|
||||
mLayoutDummy = itemView.findViewById(R.id.key_list_item_dummy);
|
||||
mMainUserId = (TextView) itemView.findViewById(R.id.key_list_item_name);
|
||||
mMainUserIdRest = (TextView) itemView.findViewById(R.id.key_list_item_email);
|
||||
mStatus = (ImageView) itemView.findViewById(R.id.key_list_item_status_icon);
|
||||
mSlinger = itemView.findViewById(R.id.key_list_item_slinger_view);
|
||||
mSlingerButton = (ImageButton) itemView.findViewById(R.id.key_list_item_slinger_button);
|
||||
mCreationDate = (TextView) itemView.findViewById(R.id.key_list_item_creation);
|
||||
}
|
||||
|
||||
public void bindKey(KeyItem keyItem, Highlighter highlighter) {
|
||||
Context ctx = itemView.getContext();
|
||||
|
||||
mLayoutData.setVisibility(View.VISIBLE);
|
||||
mLayoutDummy.setVisibility(View.GONE);
|
||||
|
||||
{ // set name and stuff, common to both key types
|
||||
OpenPgpUtils.UserId userIdSplit = keyItem.mUserId;
|
||||
if (userIdSplit.name != null) {
|
||||
mMainUserId.setText(highlighter.highlight(userIdSplit.name));
|
||||
} else {
|
||||
mMainUserId.setText(R.string.user_id_no_name);
|
||||
}
|
||||
if (userIdSplit.email != null) {
|
||||
mMainUserIdRest.setText(highlighter.highlight(userIdSplit.email));
|
||||
mMainUserIdRest.setVisibility(View.VISIBLE);
|
||||
} else {
|
||||
mMainUserIdRest.setVisibility(View.GONE);
|
||||
}
|
||||
}
|
||||
|
||||
// sort of a hack: if this item isn't enabled, we make it clickable
|
||||
// to intercept its click events. either way, no listener!
|
||||
itemView.setClickable(false);
|
||||
|
||||
{ // set edit button and status, specific by key type
|
||||
|
||||
mMasterKeyId = keyItem.mKeyId;
|
||||
|
||||
int textColor;
|
||||
|
||||
// Note: order is important!
|
||||
if (keyItem.mIsRevoked) {
|
||||
KeyFormattingUtils
|
||||
.setStatusImage(ctx, mStatus, null, KeyFormattingUtils.State.REVOKED, R.color.key_flag_gray);
|
||||
mStatus.setVisibility(View.VISIBLE);
|
||||
mSlinger.setVisibility(View.GONE);
|
||||
textColor = ctx.getResources().getColor(R.color.key_flag_gray);
|
||||
} else if (keyItem.mIsExpired) {
|
||||
KeyFormattingUtils.setStatusImage(ctx, mStatus, null, KeyFormattingUtils.State.EXPIRED, R.color.key_flag_gray);
|
||||
mStatus.setVisibility(View.VISIBLE);
|
||||
mSlinger.setVisibility(View.GONE);
|
||||
textColor = ctx.getResources().getColor(R.color.key_flag_gray);
|
||||
} else if (keyItem.mIsSecret) {
|
||||
mStatus.setVisibility(View.GONE);
|
||||
if (mSlingerButton.hasOnClickListeners()) {
|
||||
mSlingerButton.setColorFilter(
|
||||
FormattingUtils.getColorFromAttr(ctx, R.attr.colorTertiaryText),
|
||||
PorterDuff.Mode.SRC_IN);
|
||||
mSlinger.setVisibility(View.VISIBLE);
|
||||
} else {
|
||||
mSlinger.setVisibility(View.GONE);
|
||||
}
|
||||
textColor = FormattingUtils.getColorFromAttr(ctx, R.attr.colorText);
|
||||
} else {
|
||||
// this is a public key - show if it's verified
|
||||
if (keyItem.mIsVerified) {
|
||||
KeyFormattingUtils.setStatusImage(ctx, mStatus, KeyFormattingUtils.State.VERIFIED);
|
||||
mStatus.setVisibility(View.VISIBLE);
|
||||
} else {
|
||||
KeyFormattingUtils.setStatusImage(ctx, mStatus, KeyFormattingUtils.State.UNVERIFIED);
|
||||
mStatus.setVisibility(View.VISIBLE);
|
||||
}
|
||||
mSlinger.setVisibility(View.GONE);
|
||||
textColor = FormattingUtils.getColorFromAttr(ctx, R.attr.colorText);
|
||||
}
|
||||
|
||||
mMainUserId.setTextColor(textColor);
|
||||
mMainUserIdRest.setTextColor(textColor);
|
||||
|
||||
if (keyItem.mHasDuplicate) {
|
||||
String dateTime = DateUtils.formatDateTime(ctx,
|
||||
keyItem.mCreation.getTime(),
|
||||
DateUtils.FORMAT_SHOW_DATE
|
||||
| DateUtils.FORMAT_SHOW_TIME
|
||||
| DateUtils.FORMAT_SHOW_YEAR
|
||||
| DateUtils.FORMAT_ABBREV_MONTH);
|
||||
mCreationDate.setText(ctx.getString(R.string.label_key_created,
|
||||
dateTime));
|
||||
mCreationDate.setTextColor(textColor);
|
||||
mCreationDate.setVisibility(View.VISIBLE);
|
||||
} else {
|
||||
mCreationDate.setVisibility(View.GONE);
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
public void bindDummy() {
|
||||
// just reset everything to display the dummy layout
|
||||
mLayoutDummy.setVisibility(View.VISIBLE);
|
||||
mLayoutData.setVisibility(View.GONE);
|
||||
mSlinger.setVisibility(View.GONE);
|
||||
mStatus.setVisibility(View.GONE);
|
||||
itemView.setClickable(false);
|
||||
}
|
||||
}
|
||||
|
||||
static class KeyHeaderViewHolder extends RecyclerView.ViewHolder {
|
||||
private TextView mHeaderText;
|
||||
private TextView mHeaderCount;
|
||||
|
||||
public KeyHeaderViewHolder(View itemView) {
|
||||
super(itemView);
|
||||
|
||||
mHeaderText = (TextView) itemView.findViewById(R.id.stickylist_header_text);
|
||||
mHeaderCount = (TextView) itemView.findViewById(R.id.contacts_num);
|
||||
}
|
||||
|
||||
public void bind(String title, int count) {
|
||||
mHeaderText.setText(title);
|
||||
|
||||
String contactsTotal = itemView.getResources()
|
||||
.getQuantityString(R.plurals.n_keys, count, count);
|
||||
|
||||
mHeaderCount.setText(contactsTotal);
|
||||
mHeaderCount.setVisibility(View.VISIBLE);
|
||||
|
||||
}
|
||||
|
||||
public void bind(String title) {
|
||||
mHeaderText.setText(title);
|
||||
mHeaderCount.setVisibility(View.GONE);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,351 @@
|
||||
package org.sufficientlysecure.keychain.ui.util.adapter;
|
||||
|
||||
import android.content.Context;
|
||||
import android.database.Cursor;
|
||||
import android.support.v4.util.SparseArrayCompat;
|
||||
import android.support.v7.widget.RecyclerView;
|
||||
import android.view.ViewGroup;
|
||||
import android.widget.SectionIndexer;
|
||||
|
||||
import com.tonicartos.superslim.GridSLM;
|
||||
import com.tonicartos.superslim.LayoutManager;
|
||||
import com.tonicartos.superslim.LinearSLM;
|
||||
|
||||
import org.sufficientlysecure.keychain.util.Log;
|
||||
|
||||
import java.lang.annotation.Inherited;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
/**
|
||||
* @param <T> section type.
|
||||
* @param <VH> the view holder extending {@code BaseViewHolder<Cursor>} that is bound to the cursor data.
|
||||
* @param <SH> the view holder extending {@code BaseViewHolder<<T>>} that is bound to the section data.
|
||||
*/
|
||||
public abstract class SectionCursorAdapter<T, VH extends RecyclerView.ViewHolder, SH extends RecyclerView.ViewHolder>
|
||||
extends CursorAdapter implements SectionIndexer {
|
||||
|
||||
public static final String TAG = "SectionCursorAdapter";
|
||||
|
||||
private static final int VIEW_TYPE_ITEM = 0x0;
|
||||
private static final int VIEW_TYPE_SECTION = 0x1;
|
||||
|
||||
private SparseArrayCompat<T> mSectionMap = new SparseArrayCompat<>();
|
||||
private ArrayList<Integer> mSectionIndexList = new ArrayList<>();
|
||||
private Comparator<T> mSectionComparator;
|
||||
private Object[] mFastScrollItems;
|
||||
|
||||
public SectionCursorAdapter(Context context, Cursor cursor, int flags) {
|
||||
this(context, cursor, flags, new Comparator<T>() {
|
||||
@Override
|
||||
public boolean equal(T obj1, T obj2) {
|
||||
return (obj1 == null) ?
|
||||
obj2 == null : obj1.equals(obj2);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public SectionCursorAdapter(Context context, Cursor cursor, int flags, Comparator<T> comparator) {
|
||||
super(context, cursor, flags);
|
||||
setSectionComparator(comparator);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onContentChanged() {
|
||||
if (hasValidData()) {
|
||||
buildSections();
|
||||
} else {
|
||||
mSectionMap.clear();
|
||||
mSectionIndexList.clear();
|
||||
mFastScrollItems = null;
|
||||
}
|
||||
|
||||
super.onContentChanged();
|
||||
}
|
||||
|
||||
/**
|
||||
* Assign a comparator which will be used to check whether
|
||||
* a section is contained in the list of sections. The default implementation
|
||||
* will check for null pointers and compare sections using the {@link #equals(Object)} method.
|
||||
* @param comparator The comparator to compare section objects.
|
||||
*/
|
||||
public void setSectionComparator(Comparator<T> comparator) {
|
||||
this.mSectionComparator = comparator;
|
||||
buildSections();
|
||||
}
|
||||
|
||||
/**
|
||||
* If the adapter's cursor is not null then this method will call buildSections(Cursor cursor).
|
||||
*/
|
||||
private void buildSections() {
|
||||
if (hasValidData()) {
|
||||
moveCursor(-1);
|
||||
try {
|
||||
mSectionMap.clear();
|
||||
mSectionIndexList.clear();
|
||||
mFastScrollItems = null;
|
||||
|
||||
appendSections(getCursor());
|
||||
} catch (IllegalStateException e) {
|
||||
Log.e(TAG, "Couldn't build sections. Perhaps you're moving the cursor" +
|
||||
"in #getSectionFromCursor(Cursor)?", e);
|
||||
swapCursor(null);
|
||||
|
||||
mSectionMap.clear();
|
||||
mSectionIndexList.clear();
|
||||
mFastScrollItems = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
protected void appendSections(Cursor cursor) throws IllegalStateException {
|
||||
int cursorPosition = 0;
|
||||
while(hasValidData() && cursor.moveToNext()) {
|
||||
T section = getSectionFromCursor(cursor);
|
||||
if (cursor.getPosition() != cursorPosition)
|
||||
throw new IllegalStateException("Do not move the cursor's position in getSectionFromCursor.");
|
||||
if (!hasSection(section))
|
||||
mSectionMap.append(cursorPosition + mSectionMap.size(), section);
|
||||
cursorPosition++;
|
||||
}
|
||||
}
|
||||
|
||||
public boolean hasSection(T section) {
|
||||
for(int i = 0; i < mSectionMap.size(); i++) {
|
||||
T obj = mSectionMap.valueAt(i);
|
||||
if(mSectionComparator.equal(obj, section))
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* The object which is return will determine what section this cursor position will be in.
|
||||
* @return the section from the cursor at its current position.
|
||||
* This object will be passed to newSectionView and bindSectionView.
|
||||
*/
|
||||
protected abstract T getSectionFromCursor(Cursor cursor) throws IllegalStateException;
|
||||
protected String getTitleFromSection(T section) {
|
||||
return section != null ? section.toString() : "";
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getItemCount() {
|
||||
return super.getItemCount() + mSectionMap.size();
|
||||
}
|
||||
|
||||
@Override
|
||||
public final long getItemId(int listPosition) {
|
||||
if (isSection(listPosition))
|
||||
return listPosition;
|
||||
else {
|
||||
int cursorPosition = getCursorPositionWithoutSections(listPosition);
|
||||
return super.getItemId(cursorPosition);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param listPosition the position of the current item in the list with mSectionMap included
|
||||
* @return Whether or not the listPosition points to a section.
|
||||
*/
|
||||
public boolean isSection(int listPosition) {
|
||||
return mSectionMap.indexOfKey(listPosition) >= 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* This will map a position in the list adapter (which includes mSectionMap) to a position in
|
||||
* the cursor (which does not contain mSectionMap).
|
||||
*
|
||||
* @param listPosition the position of the current item in the list with mSectionMap included
|
||||
* @return the correct position to use with the cursor
|
||||
*/
|
||||
public int getCursorPositionWithoutSections(int listPosition) {
|
||||
if (mSectionMap.size() == 0) {
|
||||
return listPosition;
|
||||
} else if (!isSection(listPosition)) {
|
||||
int sectionIndex = getSectionForPosition(listPosition);
|
||||
if (isListPositionBeforeFirstSection(listPosition, sectionIndex)) {
|
||||
return listPosition;
|
||||
} else {
|
||||
return listPosition - (sectionIndex + 1);
|
||||
}
|
||||
} else {
|
||||
return -1;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the section object for the index within the array of sections.
|
||||
* @param sectionPosition The section index.
|
||||
* @return The specified section object for this position.
|
||||
*/
|
||||
public T getSection(int sectionPosition) {
|
||||
if (mSectionIndexList.contains(sectionPosition)) {
|
||||
return mSectionMap.get(mSectionIndexList.get(sectionPosition));
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns all indices at which the first item of a section is placed.
|
||||
* @return The first index of each section.
|
||||
*/
|
||||
public List<Integer> getSectionListPositions() {
|
||||
return mSectionIndexList;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
@Override
|
||||
public int getPositionForSection(int sectionIndex) {
|
||||
if (mSectionIndexList.isEmpty()) {
|
||||
for (int i = 0; i < mSectionMap.size(); i++) {
|
||||
mSectionIndexList.add(mSectionMap.keyAt(i));
|
||||
}
|
||||
}
|
||||
|
||||
return sectionIndex < mSectionIndexList.size() ?
|
||||
mSectionIndexList.get(sectionIndex) : getItemCount();
|
||||
}
|
||||
|
||||
/**
|
||||
* Given the position of a section, returns its index in the array of sections.
|
||||
* @param sectionPosition The first position of the corresponding section in the array of items.
|
||||
* @return The section index in the array of sections.
|
||||
*/
|
||||
public int getSectionIndexForPosition(int sectionPosition) {
|
||||
if (mSectionIndexList.isEmpty()) {
|
||||
for (int i = 0; i < mSectionMap.size(); i++) {
|
||||
mSectionIndexList.add(mSectionMap.keyAt(i));
|
||||
}
|
||||
}
|
||||
|
||||
return mSectionIndexList.indexOf(sectionPosition);
|
||||
}
|
||||
|
||||
/**
|
||||
* Given the list position of an item in the adapter, returns the
|
||||
* adapter position of the first item of the section the given item belongs to.
|
||||
* @param listPosition The absolute list position.
|
||||
* @return The position of the first item of the section.
|
||||
*/
|
||||
public int getFirstSectionPosition(int listPosition) {
|
||||
int start = 0;
|
||||
for(int i = 0; i <= listPosition; i++) {
|
||||
if(isSection(i)) {
|
||||
start = i;
|
||||
}
|
||||
}
|
||||
|
||||
return start;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
@Override
|
||||
public int getSectionForPosition(int listPosition) {
|
||||
boolean isSection = false;
|
||||
int numPrecedingSections = 0;
|
||||
for (int i = 0; i < mSectionMap.size(); i++) {
|
||||
int sectionPosition = mSectionMap.keyAt(i);
|
||||
|
||||
if (listPosition > sectionPosition) {
|
||||
numPrecedingSections++;
|
||||
} else if (listPosition == sectionPosition) {
|
||||
isSection = true;
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return isSection ? numPrecedingSections : Math.max(numPrecedingSections - 1, 0);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Object[] getSections() {
|
||||
if(mFastScrollItems == null) {
|
||||
mFastScrollItems = getSectionLabels();
|
||||
}
|
||||
|
||||
return mFastScrollItems;
|
||||
}
|
||||
|
||||
private Object[] getSectionLabels() {
|
||||
if(mSectionMap == null)
|
||||
return new Object[0];
|
||||
|
||||
String[] ret = new String[mSectionMap.size()];
|
||||
for(int i = 0; i < ret.length; i++) {
|
||||
ret[i] = getTitleFromSection(mSectionMap.valueAt(i));
|
||||
}
|
||||
|
||||
return ret;
|
||||
}
|
||||
|
||||
private boolean isListPositionBeforeFirstSection(int listPosition, int sectionIndex) {
|
||||
boolean hasSections = mSectionMap != null && mSectionMap.size() > 0;
|
||||
return sectionIndex == 0 && hasSections && listPosition < mSectionMap.keyAt(0);
|
||||
}
|
||||
|
||||
@Override
|
||||
public final int getItemViewType(int listPosition) {
|
||||
return isSection(listPosition) ? VIEW_TYPE_SECTION : VIEW_TYPE_ITEM;
|
||||
}
|
||||
|
||||
@Override
|
||||
@SuppressWarnings("unchecked")
|
||||
public final void onBindViewHolder(RecyclerView.ViewHolder holder, int position) {
|
||||
LayoutManager.LayoutParams layoutParams = LayoutManager.LayoutParams
|
||||
.from(holder.itemView.getLayoutParams());
|
||||
|
||||
// assign first position of section to each item
|
||||
layoutParams.setFirstPosition(getFirstSectionPosition(position));
|
||||
|
||||
switch (holder.getItemViewType()) {
|
||||
case VIEW_TYPE_ITEM :
|
||||
moveCursorOrThrow(getCursorPositionWithoutSections(position));
|
||||
onBindItemViewHolder((VH) holder, getCursor());
|
||||
|
||||
layoutParams.isHeader = false;
|
||||
break;
|
||||
|
||||
case VIEW_TYPE_SECTION:
|
||||
T section = mSectionMap.get(position);
|
||||
int sectionIndex = getSectionIndexForPosition(position);
|
||||
onBindSectionViewHolder((SH) holder, sectionIndex, section);
|
||||
|
||||
layoutParams.isHeader = true;
|
||||
break;
|
||||
}
|
||||
|
||||
holder.itemView.setLayoutParams(layoutParams);
|
||||
}
|
||||
|
||||
@Override
|
||||
public final RecyclerView.ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
|
||||
switch (viewType) {
|
||||
case VIEW_TYPE_SECTION:
|
||||
return onCreateSectionViewHolder(parent);
|
||||
|
||||
case VIEW_TYPE_ITEM:
|
||||
return onCreateItemViewHolder(parent, viewType);
|
||||
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
protected abstract SH onCreateSectionViewHolder(ViewGroup parent);
|
||||
protected abstract VH onCreateItemViewHolder(ViewGroup parent, int viewType);
|
||||
|
||||
protected abstract void onBindSectionViewHolder(SH holder, int sectionIndex, T section);
|
||||
protected abstract void onBindItemViewHolder(VH holder, Cursor cursor);
|
||||
|
||||
public interface Comparator<T> {
|
||||
boolean equal(T obj1, T obj2);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -12,12 +12,10 @@
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent">
|
||||
|
||||
<se.emilsjolander.stickylistheaders.StickyListHeadersListView
|
||||
<android.support.v7.widget.RecyclerView
|
||||
android:id="@+id/key_list_list"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:drawSelectorOnTop="true"
|
||||
android:fastScrollEnabled="true"
|
||||
android:paddingLeft="16dp"
|
||||
android:paddingRight="32dp"
|
||||
android:scrollbarStyle="outsideOverlay" />
|
||||
|
||||
@@ -1,7 +1,16 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
<RelativeLayout
|
||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:super="http://schemas.android.com/apk/lib-auto"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content" >
|
||||
android:layout_height="wrap_content"
|
||||
android:background="?android:colorBackground"
|
||||
|
||||
super:slm_headerDisplay="sticky|inline"
|
||||
super:slm_section_sectionManager="linear"
|
||||
tools:ignore="ResAuto">
|
||||
|
||||
<TextView
|
||||
style="@style/SectionHeader"
|
||||
|
||||
Reference in New Issue
Block a user