From 930ea073b326245bd411cd5acbf9cb343a0b24da Mon Sep 17 00:00:00 2001 From: Tobias Erthal Date: Tue, 6 Sep 2016 01:40:16 +0200 Subject: [PATCH] Improved SectionedCursorAdapter, added support for the dummy key item view. --- .../keychain/ui/KeyListFragment.java | 83 ++-- .../keychain/ui/adapter/KeyAdapter.java | 23 - .../adapter/KeySectionedListAdapter.java | 238 +++++++---- .../keychain/ui/util/adapter/KeyAdapter.java | 16 +- .../ui/util/adapter/SectionCursorAdapter.java | 158 +++---- .../util/recyclerview/RecyclerFragment.java | 397 ++++++++++++++++++ .../src/main/res/layout/key_list_dummy.xml | 47 +++ .../src/main/res/layout/key_list_fragment.xml | 209 +++++---- ...header.xml => key_list_header_private.xml} | 14 +- .../res/layout/key_list_header_public.xml | 22 + .../src/main/res/layout/key_list_item.xml | 64 +-- 11 files changed, 845 insertions(+), 426 deletions(-) rename OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/{util => }/adapter/KeySectionedListAdapter.java (56%) create mode 100644 OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/util/recyclerview/RecyclerFragment.java create mode 100644 OpenKeychain/src/main/res/layout/key_list_dummy.xml rename OpenKeychain/src/main/res/layout/{key_list_header.xml => key_list_header_private.xml} (76%) create mode 100644 OpenKeychain/src/main/res/layout/key_list_header_public.xml diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/KeyListFragment.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/KeyListFragment.java index 4ee821322..4ed45832d 100644 --- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/KeyListFragment.java +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/KeyListFragment.java @@ -21,12 +21,10 @@ package org.sufficientlysecure.keychain.ui; import android.animation.ObjectAnimator; import android.annotation.TargetApi; import android.app.Activity; -import android.content.Context; import android.content.Intent; import android.database.Cursor; import android.database.MatrixCursor; import android.database.MergeCursor; -import android.graphics.Color; import android.net.Uri; import android.os.Build; import android.os.Bundle; @@ -35,7 +33,6 @@ 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; @@ -46,11 +43,8 @@ import android.view.MenuItem; import android.view.View; import android.view.View.OnClickListener; import android.view.ViewGroup; -import android.widget.AbsListView.MultiChoiceModeListener; import android.widget.AdapterView; import android.widget.Button; -import android.widget.ListView; -import android.widget.TextView; import android.widget.ViewAnimator; import com.getbase.floatingactionbutton.FloatingActionButton; @@ -73,26 +67,22 @@ import org.sufficientlysecure.keychain.service.ConsolidateInputParcel; import org.sufficientlysecure.keychain.service.ImportKeyringParcel; import org.sufficientlysecure.keychain.ui.adapter.KeyAdapter; import org.sufficientlysecure.keychain.ui.base.CryptoOperationHelper; -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.ui.adapter.KeySectionedListAdapter; +import org.sufficientlysecure.keychain.ui.util.recyclerview.RecyclerFragment; import org.sufficientlysecure.keychain.util.FabContainer; import org.sufficientlysecure.keychain.util.Log; import org.sufficientlysecure.keychain.util.Preferences; -import se.emilsjolander.stickylistheaders.StickyListHeadersAdapter; -import se.emilsjolander.stickylistheaders.StickyListHeadersListView; import java.io.IOException; import java.util.ArrayList; -import java.util.HashMap; /** * Public key list with sticky list headers. It does _not_ extend ListFragment because it uses * StickyListHeaders library which does not extend upon ListView. */ -public class KeyListFragment extends LoaderFragment +public class KeyListFragment extends RecyclerFragment implements SearchView.OnQueryTextListener, AdapterView.OnItemClickListener, LoaderManager.LoaderCallbacks, FabContainer, CryptoOperationHelper.Callback { @@ -101,10 +91,6 @@ public class KeyListFragment extends LoaderFragment private static final int REQUEST_DELETE = 2; private static final int REQUEST_VIEW_KEY = 3; - //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; @@ -126,12 +112,8 @@ public class KeyListFragment extends LoaderFragment * Load custom layout with StickyListView from library */ @Override - public View onCreateView(LayoutInflater inflater, ViewGroup superContainer, Bundle savedInstanceState) { - View root = super.onCreateView(inflater, superContainer, savedInstanceState); - View view = inflater.inflate(R.layout.key_list_fragment, getContainer()); - - mStickyList = (RecyclerView) view.findViewById(R.id.key_list_list); - //mStickyList.setOnItemClickListener(this); + public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { + View view = inflater.inflate(R.layout.key_list_fragment, container, false); mFab = (FloatingActionsMenu) view.findViewById(R.id.fab_main); @@ -162,7 +144,7 @@ public class KeyListFragment extends LoaderFragment }); - return root; + return view; } /** @@ -182,7 +164,7 @@ public class KeyListFragment extends LoaderFragment //mStickyList.setDrawingListUnderStickyHeader(false); //mStickyList.setFastScrollEnabled(true); - // Adds an empty footer view so that the Floating Action Button won't block content + /* Adds an empty footer view so that the Floating Action Button won't block content // in last few rows. View footer = new View(activity); @@ -197,6 +179,7 @@ public class KeyListFragment extends LoaderFragment footer.setLayoutParams(params); //mStickyList.addFooterView(footer, null, false); + */ /* * Multi-selection @@ -269,7 +252,7 @@ public class KeyListFragment extends LoaderFragment setHasOptionsMenu(true); // Start out with a progress indicator. - setContentShown(false); + hideList(true); // this view is made visible if no data is available // mStickyList.setEmptyView(activity.findViewById(R.id.key_list_empty)); @@ -286,10 +269,9 @@ 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 KeySectionedListAdapter(getContext(), null); - mStickyList.setAdapter(mAdapter); - mStickyList.setLayoutManager(new LayoutManager(getActivity())); + setAdapter(new KeySectionedListAdapter(getContext(), null)); + setLayoutManager(new LayoutManager(getActivity())); // Prepare the loader. Either re-connect with an existing one, // or start a new one. @@ -324,22 +306,22 @@ public class KeyListFragment extends LoaderFragment // Now create and return a CursorLoader that will take care of // creating a Cursor for the data being displayed. - return new CursorLoader(getActivity(), uri, KeyListAdapter.PROJECTION, null, null, ORDER); + return new CursorLoader(getActivity(), uri, KeyAdapter.PROJECTION, null, null, ORDER); } @Override public void onLoadFinished(Loader loader, Cursor data) { // Swap the new cursor in. (The framework will take care of closing the // old cursor once we return.) - mAdapter.setSearchQuery(mQuery); + getAdapter().setSearchQuery(mQuery); if (data != null && (mQuery == null || TextUtils.isEmpty(mQuery))) { - boolean isSecret = data.moveToFirst() && data.getInt(KeyListAdapter.INDEX_HAS_ANY_SECRET) != 0; + boolean isSecret = data.moveToFirst() && data.getInt(KeyAdapter.INDEX_HAS_ANY_SECRET) != 0; if (!isSecret) { - MatrixCursor headerCursor = new MatrixCursor(KeyListAdapter.PROJECTION); - Long[] row = new Long[KeyListAdapter.PROJECTION.length]; - row[KeyListAdapter.INDEX_HAS_ANY_SECRET] = 1L; - row[KeyListAdapter.INDEX_MASTER_KEY_ID] = 0L; + MatrixCursor headerCursor = new MatrixCursor(KeyAdapter.PROJECTION); + Long[] row = new Long[KeyAdapter.PROJECTION.length]; + row[KeyAdapter.INDEX_HAS_ANY_SECRET] = 1L; + row[KeyAdapter.INDEX_MASTER_KEY_ID] = 0L; headerCursor.addRow(row); Cursor dataCursor = data; @@ -348,7 +330,8 @@ public class KeyListFragment extends LoaderFragment }); } } - mAdapter.swapCursor(data); + + getAdapter().swapCursor(data); // end action mode, if any if (mActionMode != null) { @@ -357,9 +340,9 @@ public class KeyListFragment extends LoaderFragment // The list should now be shown. if (isResumed()) { - setContentShown(true); + showList(true); } else { - setContentShownNoAnimation(true); + showList(false); } } @@ -368,7 +351,7 @@ public class KeyListFragment extends LoaderFragment // This is called when the last Cursor provided to onLoadFinished() // above is about to be closed. We need to make sure we are no // longer using it. - mAdapter.swapCursor(null); + getAdapter().swapCursor(null); } /** @@ -378,7 +361,7 @@ public class KeyListFragment extends LoaderFragment public void onItemClick(AdapterView adapterView, View view, int position, long id) { Intent viewIntent = new Intent(getActivity(), ViewKeyActivity.class); viewIntent.setData( - KeyRings.buildGenericKeyRingUri(mAdapter.getMasterKeyId(position))); + KeyRings.buildGenericKeyRingUri(getAdapter().getMasterKeyId(position))); startActivityForResult(viewIntent, REQUEST_VIEW_KEY); } @@ -762,6 +745,7 @@ public class KeyListFragment extends LoaderFragment return false; } + /* public class KeyListAdapter extends KeyAdapter implements StickyListHeadersAdapter { private HashMap mSelection = new HashMap<>(); @@ -842,19 +826,13 @@ public class KeyListFragment extends LoaderFragment TextView mCount; } - /** - * Creates a new header view and binds the section headers to it. It uses the ViewHolder - * pattern. Most functionality is similar to getView() from Android's CursorAdapter. - *

- * NOTE: The variables mDataValid and mCursor are available due to the super class - * CursorAdapter. - */ + @Override public View getHeaderView(int position, View convertView, ViewGroup parent) { HeaderViewHolder holder; if (convertView == null) { holder = new HeaderViewHolder(); - convertView = mInflater.inflate(R.layout.key_list_header, parent, false); + convertView = mInflater.inflate(R.layout.key_list_header_public, parent, false); holder.mText = (TextView) convertView.findViewById(R.id.stickylist_header_text); holder.mCount = (TextView) convertView.findViewById(R.id.contacts_num); convertView.setTag(holder); @@ -899,9 +877,6 @@ public class KeyListFragment extends LoaderFragment return convertView; } - /** - * Header IDs should be static, position=1 should always return the same Id that is. - */ @Override public long getHeaderId(int position) { if (!mDataValid) { @@ -927,9 +902,6 @@ public class KeyListFragment extends LoaderFragment } } - /** - * -------------------------- MULTI-SELECTION METHODS -------------- - */ public void setNewSelection(int position, boolean value) { mSelection.put(position, value); notifyDataSetChanged(); @@ -965,5 +937,6 @@ public class KeyListFragment extends LoaderFragment } } +*/ } diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/adapter/KeyAdapter.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/adapter/KeyAdapter.java index 56dd15a8f..cb02d4b6b 100644 --- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/adapter/KeyAdapter.java +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/adapter/KeyAdapter.java @@ -94,7 +94,6 @@ public class KeyAdapter extends CursorAdapter { public static class KeyItemViewHolder { public View mView; - public View mLayoutDummy; public View mLayoutData; public Long mMasterKeyId; public TextView mMainUserId; @@ -109,7 +108,6 @@ public class KeyAdapter extends CursorAdapter { public KeyItemViewHolder(View view) { mView = view; mLayoutData = view.findViewById(R.id.key_list_item_data); - mLayoutDummy = view.findViewById(R.id.key_list_item_dummy); mMainUserId = (TextView) view.findViewById(R.id.key_list_item_name); mMainUserIdRest = (TextView) view.findViewById(R.id.key_list_item_email); mStatus = (ImageView) view.findViewById(R.id.key_list_item_status_icon); @@ -119,10 +117,6 @@ public class KeyAdapter extends CursorAdapter { } public void setData(Context context, KeyItem item, Highlighter highlighter, boolean enabled) { - - mLayoutData.setVisibility(View.VISIBLE); - mLayoutDummy.setVisibility(View.GONE); - mDisplayedItem = item; { // set name and stuff, common to both key types @@ -207,25 +201,8 @@ public class KeyAdapter extends CursorAdapter { } else { mCreationDate.setVisibility(View.GONE); } - } - } - - /** Shows the "you have no keys yet" dummy view, and sets an OnClickListener. */ - public void setDummy(OnClickListener listener) { - - // just reset everything to display the dummy layout - mLayoutDummy.setVisibility(View.VISIBLE); - mLayoutData.setVisibility(View.GONE); - mSlinger.setVisibility(View.GONE); - mStatus.setVisibility(View.GONE); - mView.setClickable(false); - - mLayoutDummy.setOnClickListener(listener); - - } - } public boolean isEnabled(Cursor cursor) { diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/util/adapter/KeySectionedListAdapter.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/adapter/KeySectionedListAdapter.java similarity index 56% rename from OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/util/adapter/KeySectionedListAdapter.java rename to OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/adapter/KeySectionedListAdapter.java index 673f76928..420e88214 100644 --- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/util/adapter/KeySectionedListAdapter.java +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/adapter/KeySectionedListAdapter.java @@ -1,9 +1,10 @@ -package org.sufficientlysecure.keychain.ui.util.adapter; +package org.sufficientlysecure.keychain.ui.adapter; import android.content.Context; import android.database.Cursor; import android.graphics.PorterDuff; -import android.support.v7.widget.RecyclerView; +import android.support.v4.content.ContextCompat; +import android.text.TextUtils; import android.text.format.DateUtils; import android.util.SparseBooleanArray; import android.view.LayoutInflater; @@ -14,15 +15,24 @@ import android.widget.ImageView; import android.widget.TextView; import org.openintents.openpgp.util.OpenPgpUtils; +import org.sufficientlysecure.keychain.Constants; 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; +import org.sufficientlysecure.keychain.ui.util.adapter.*; +import org.sufficientlysecure.keychain.util.Log; + +public class KeySectionedListAdapter extends SectionCursorAdapter implements org.sufficientlysecure.keychain.ui.util.adapter.KeyAdapter { + private static final short VIEW_ITEM_TYPE_KEY = 0x0; + private static final short VIEW_ITEM_TYPE_DUMMY = 0x1; + + private static final short VIEW_SECTION_TYPE_PRIVATE = 0x0; + private static final short VIEW_SECTION_TYPE_PUBLIC = 0x1; -public class KeySectionedListAdapter extends SectionCursorAdapter implements KeyAdapter { private String mQuery; private SparseBooleanArray mSelectionMap; - + private boolean mHasDummy = false; public KeySectionedListAdapter(Context context, Cursor cursor) { super(context, cursor, 0); @@ -67,57 +77,148 @@ public class KeySectionedListAdapter extends SectionCursorAdapter 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()); + return '#'; } else { - holder.bind(section); + String userId = cursor.getString(INDEX_USER_ID); + if(TextUtils.isEmpty(userId)) { + return '?'; + } else { + return Character.toUpperCase(userId.charAt(0)); + } } } @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(); + protected short getSectionHeaderViewType(int sectionIndex) { + return (sectionIndex < 1) ? + VIEW_SECTION_TYPE_PRIVATE : + VIEW_SECTION_TYPE_PUBLIC; + } + + @Override + protected short getSectionItemViewType(int position) { + if(moveCursor(position)) { + boolean hasMaster = getCursor().getLong(INDEX_MASTER_KEY_ID) != 0L; + boolean isSecret = getCursor().getInt(INDEX_HAS_ANY_SECRET) != 0; + + if (isSecret && !hasMaster) { + return VIEW_ITEM_TYPE_DUMMY; + } } else { + Log.w(Constants.TAG, "Unable to determine key view type. " + + "Reason: Could not move cursor over dataset."); + } + + return VIEW_ITEM_TYPE_KEY; + } + + @Override + protected KeyHeaderViewHolder onCreateSectionViewHolder(ViewGroup parent, int viewType) { + switch (viewType) { + case VIEW_SECTION_TYPE_PUBLIC: + return new KeyHeaderViewHolder(LayoutInflater.from(parent.getContext()) + .inflate(R.layout.key_list_header_public, parent, false)); + + case VIEW_SECTION_TYPE_PRIVATE: + return new KeyHeaderViewHolder(LayoutInflater.from(parent.getContext()) + .inflate(R.layout.key_list_header_private, parent, false)); + + default: + return null; + } + } + + @Override + protected ViewHolder onCreateItemViewHolder(ViewGroup parent, int viewType) { + switch (viewType) { + case VIEW_ITEM_TYPE_KEY: + return new KeyItemViewHolder(LayoutInflater.from(parent.getContext()) + .inflate(R.layout.key_list_item, parent, false)); + + case VIEW_ITEM_TYPE_DUMMY: + return new KeyDummyViewHolder(LayoutInflater.from(parent.getContext()) + .inflate(R.layout.key_list_dummy, parent, false)); + + default: + return null; + } + + } + + @Override + protected void onBindSectionViewHolder(KeyHeaderViewHolder holder, Character section) { + switch (holder.getItemViewTypeWithoutSections()) { + case VIEW_SECTION_TYPE_PUBLIC: { + String title = section.equals('?') ? + getContext().getString(R.string.user_id_no_name) : + String.valueOf(section); + + holder.bind(title); + break; + } + + case VIEW_SECTION_TYPE_PRIVATE: { + int count = getCount(); + String title = getContext().getResources() + .getQuantityString(R.plurals.n_keys, count, count); + holder.bind(title); + break; + } + + } + } + + @Override + protected void onBindItemViewHolder(ViewHolder holder, Cursor cursor) { + if (holder.getItemViewTypeWithoutSections() == VIEW_ITEM_TYPE_KEY) { Highlighter highlighter = new Highlighter(getContext(), mQuery); - holder.bindKey(new KeyItem(cursor), highlighter); + ((KeyItemViewHolder) holder).bindKey(new KeyItem(cursor), highlighter); } } - static class KeyItemViewHolder extends RecyclerView.ViewHolder { - private View mLayoutDummy; + private static class KeyDummyViewHolder extends SectionCursorAdapter.ViewHolder + implements View.OnClickListener{ + + public KeyDummyViewHolder(View itemView) { + super(itemView); + + itemView.setClickable(true); + itemView.setOnClickListener(this); + } + + @Override + public void onClick(View view) { + + } + } + + private static class KeyItemViewHolder extends SectionCursorAdapter.ViewHolder + implements View.OnClickListener, View.OnLongClickListener { + private View mLayoutData; private Long mMasterKeyId; private TextView mMainUserId; @@ -130,22 +231,23 @@ public class KeySectionedListAdapter extends SectionCursorAdapter section type. * @param the view holder extending {@code BaseViewHolder} that is bound to the cursor data. * @param the view holder extending {@code BaseViewHolder<>} that is bound to the section data. */ -public abstract class SectionCursorAdapter - extends CursorAdapter implements SectionIndexer { +public abstract class SectionCursorAdapter extends CursorAdapter { public static final String TAG = "SectionCursorAdapter"; - private static final int VIEW_TYPE_ITEM = 0x0; - private static final int VIEW_TYPE_SECTION = 0x1; + private static final short VIEW_TYPE_ITEM = 0x1; + private static final short VIEW_TYPE_SECTION = 0x2; private SparseArrayCompat mSectionMap = new SparseArrayCompat<>(); - private ArrayList mSectionIndexList = new ArrayList<>(); private Comparator mSectionComparator; - private Object[] mFastScrollItems; public SectionCursorAdapter(Context context, Cursor cursor, int flags) { this(context, cursor, flags, new Comparator() { @@ -55,8 +46,6 @@ public abstract class SectionCursorAdapter 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. @@ -242,10 +173,7 @@ public abstract class SectionCursorAdapter 0; return sectionIndex == 0 && hasSections && listPosition < mSectionMap.keyAt(0); @@ -292,7 +199,21 @@ public abstract class SectionCursorAdapter> 16); case VIEW_TYPE_ITEM: - return onCreateItemViewHolder(parent, viewType); + return onCreateItemViewHolder(parent, viewType >> 16); default: return null; } } - protected abstract SH onCreateSectionViewHolder(ViewGroup parent); + protected abstract SH onCreateSectionViewHolder(ViewGroup parent, int viewType); protected abstract VH onCreateItemViewHolder(ViewGroup parent, int viewType); - protected abstract void onBindSectionViewHolder(SH holder, int sectionIndex, T section); + protected abstract void onBindSectionViewHolder(SH holder, T section); protected abstract void onBindItemViewHolder(VH holder, Cursor cursor); public interface Comparator { boolean equal(T obj1, T obj2); } + + public static class ViewHolder extends RecyclerView.ViewHolder { + public ViewHolder(View itemView) { + super(itemView); + } + + /** + * Returns the view type assigned in + * {@link SectionCursorAdapter#getSectionHeaderViewType(int)} or + * {@link SectionCursorAdapter#getSectionItemViewType(int)} + * + * Note that a call to {@link #getItemViewType()} will return a value that contains + * internal stuff necessary to distinguish sections from items. + * @return The view type you set. + */ + public short getItemViewTypeWithoutSections(){ + return (short) (getItemViewType() >> 16); + } + } } diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/util/recyclerview/RecyclerFragment.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/util/recyclerview/RecyclerFragment.java new file mode 100644 index 000000000..b6deb0362 --- /dev/null +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/util/recyclerview/RecyclerFragment.java @@ -0,0 +1,397 @@ +/* + * Implementation of taken from the sourcecode of + * android.support.v4.app.ListFragment from the + * Android Open Source Project and changed to use + * RecyclerView instead of ListView. + */ + +/* + * Copyright (C) 2013 The Android Open Source Project + * + * 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. + */ + +package org.sufficientlysecure.keychain.ui.util.recyclerview; + +import android.content.Context; +import android.os.Bundle; +import android.os.Handler; +import android.support.v4.app.Fragment; +import android.support.v7.widget.RecyclerView; +import android.view.Gravity; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.view.animation.AnimationUtils; +import android.widget.FrameLayout; +import android.widget.LinearLayout; +import android.widget.ProgressBar; +import android.widget.TextView; + +import org.sufficientlysecure.keychain.util.Log; + +public class RecyclerFragment extends Fragment { + + private static final int INTERNAL_LIST_VIEW_ID = android.R.id.list; + private static final int INTERNAL_EMPTY_VIEW_ID = android.R.id.empty; + private static final int INTERNAL_LIST_CONTAINER_ID = android.R.id.widget_frame; + private static final int INTERNAL_PROGRESS_CONTAINER_ID = android.R.id.progress; + + private final Handler handler = new Handler(); + private final Runnable requestFocus = new Runnable() { + @Override + public void run() { + listView.focusableViewAvailable(listView); + } + }; + + private boolean observerRegistered = false; + private final RecyclerView.AdapterDataObserver dataObserver = new RecyclerView.AdapterDataObserver() { + @Override + public void onChanged() { + super.onChanged(); + checkDataSet(); + } + + @Override + public void onItemRangeInserted(int positionStart, int itemCount) { + super.onItemRangeInserted(positionStart, itemCount); + checkDataSet(); + } + + @Override + public void onItemRangeRemoved(int positionStart, int itemCount) { + super.onItemRangeRemoved(positionStart, itemCount); + checkDataSet(); + } + }; + + private final RecyclerView.OnScrollListener scrollListener = new RecyclerView.OnScrollListener() { + + @Override + public void onScrolled(RecyclerView recyclerView, int dx, int dy) { + RecyclerFragment.this.onScrolled(dx, dy); + } + + @Override + public void onScrollStateChanged(RecyclerView recyclerView, int newState) { + RecyclerFragment.this.onScrollStateChanged(newState); + } + }; + + private A adapter; + private RecyclerView.LayoutManager layoutManager; + private RecyclerView listView; + private View emptyView; + private View progressContainer; + private View listContainer; + private boolean listShown; + + public RecyclerFragment() { + super(); + } + + @Override + public View onCreateView(LayoutInflater inflater, ViewGroup parent, Bundle savedInstanceState) { + + final Context context = parent.getContext(); + FrameLayout root = new FrameLayout(context); + + LinearLayout progressContainer = new LinearLayout(context); + progressContainer.setId(INTERNAL_PROGRESS_CONTAINER_ID); + progressContainer.setOrientation(LinearLayout.VERTICAL); + progressContainer.setGravity(Gravity.CENTER); + progressContainer.setVisibility(View.GONE); + + ProgressBar progressBar = new ProgressBar(context, null, + android.R.attr.progressBarStyleLarge); + + progressContainer.addView(progressBar, new LinearLayout.LayoutParams( + ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT)); + + root.addView(progressContainer, new FrameLayout.LayoutParams( + ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT)); + + + FrameLayout listContainer = new FrameLayout(context); + listContainer.setId(INTERNAL_LIST_CONTAINER_ID); + + TextView textView = new TextView(context); + textView.setId(INTERNAL_EMPTY_VIEW_ID); + textView.setGravity(Gravity.CENTER); + + listContainer.addView(textView, new FrameLayout.LayoutParams( + ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT)); + + RecyclerView listView = new RecyclerView(context); + listView.setId(INTERNAL_LIST_VIEW_ID); + + listContainer.addView(listView, new FrameLayout.LayoutParams( + ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT)); + + + root.addView(listContainer, new FrameLayout.LayoutParams( + ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT)); + + + root.setLayoutParams(new FrameLayout.LayoutParams( + ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT)); + + return root; + } + + @Override + public void onViewCreated(View view, Bundle savedInstanceState) { + super.onViewCreated(view, savedInstanceState); + ensureList(); + } + + @Override + public void onDestroyView() { + handler.removeCallbacks(requestFocus); + listView.setLayoutManager(null); + listView.removeOnScrollListener(scrollListener); + + listView = null; + listShown = false; + listContainer = null; + layoutManager = null; + emptyView = null; + progressContainer = null; + + super.onDestroyView(); + } + + @Override + public void onDestroy() { + setAdapter(null); + super.onDestroy(); + } + + public Handler getHandler() { + return handler; + } + + public void onScrollStateChanged(int state) { + // empty body + } + + public void onScrolled(int dx, int dy) { + // empty body + } + + public void setAdapter(A adapter) { + unregisterObserver(); + + boolean hadAdapter = this.adapter != null; + this.adapter = adapter; + + registerObserver(); + + if(listView != null) { + listView.setAdapter(adapter); + + if(!listShown && !hadAdapter) { + if(getView() != null) + setListShown(true, getView().getWindowToken() != null); + } + } + } + + public void setLayoutManager(RecyclerView.LayoutManager manager) { + if(!manager.isAttachedToWindow()) { + layoutManager = manager; + + if (listView != null) { + listView.setLayoutManager(manager); + } + } + } + + public int getItemCount() { + if(adapter != null) + return adapter.getItemCount(); + else + return 0; + } + + public long getItemId(int position) { + if(adapter != null) + return adapter.getItemId(position); + else + return View.NO_ID; + } + + public RecyclerView getRecyclerView() { + ensureList(); + return listView; + } + + public RecyclerView.LayoutManager getLayoutManager() { + ensureList(); + return layoutManager; + } + + private void registerObserver() { + if(!observerRegistered && adapter != null) { + adapter.registerAdapterDataObserver(dataObserver); + observerRegistered = true; + } + } + + private void unregisterObserver() { + if(observerRegistered && adapter != null) { + adapter.unregisterAdapterDataObserver(dataObserver); + observerRegistered = false; + } + } + + private void checkDataSet() { + boolean empty = treatAsEmpty(getItemCount()); + + Log.d("RecyclerFragment", "Dataset change detected! Count: " + + getItemCount() + ", Empty: " + empty); + + if(emptyView != null) { + emptyView.setVisibility(empty ? View.VISIBLE : View.GONE); + } + } + + /** + * Set whether the data set of the recycler view should be treated as empty. + * This is useful e.g. if you have an empty padding row and therefore the item + * count is always greater than 0. + * + * @param itemCount the number of items in the data set. + * @return Whether to treat this as an empty set of data + */ + protected boolean treatAsEmpty(int itemCount) { + return itemCount < 1; + } + + /** + * Set whether the recycler view should have a fixed size or not + */ + protected boolean isFixedSize() { + return true; + } + + public void hideList(boolean animated) { + setListShown(false, animated); + } + + public void showList(boolean animated) { + setListShown(true, animated); + } + + private void setListShown(boolean shown, boolean animated) { + ensureList(); + + if(progressContainer == null) + throw new IllegalStateException("Can't be used with a custom content view"); + + if(listShown == shown) + return; + + listShown = shown; + if(shown) { + if (animated) { + progressContainer.startAnimation(AnimationUtils.loadAnimation( + getActivity(), android.R.anim.fade_out)); + listContainer.startAnimation(AnimationUtils.loadAnimation( + getActivity(), android.R.anim.fade_in)); + } else { + progressContainer.clearAnimation(); + listContainer.clearAnimation(); + } + progressContainer.setVisibility(View.GONE); + listContainer.setVisibility(View.VISIBLE); + } else { + if (animated) { + progressContainer.startAnimation(AnimationUtils.loadAnimation( + getActivity(), android.R.anim.fade_in)); + listContainer.startAnimation(AnimationUtils.loadAnimation( + getActivity(), android.R.anim.fade_out)); + } else { + progressContainer.clearAnimation(); + listContainer.clearAnimation(); + } + progressContainer.setVisibility(View.VISIBLE); + listContainer.setVisibility(View.GONE); + } + } + + public A getAdapter() { + return adapter; + } + + @SuppressWarnings("unchecked") + private void ensureList() { + if (listView != null) + return; + + View root = getView(); + if (root == null) + throw new IllegalStateException("Content view not yet created"); + + if (root instanceof RecyclerView) { + listView = (RecyclerView) root; + } else { + emptyView = root.findViewById(INTERNAL_EMPTY_VIEW_ID); + if(emptyView != null) { + emptyView.setVisibility(View.GONE); + } + + progressContainer = root.findViewById(INTERNAL_PROGRESS_CONTAINER_ID); + listContainer = root.findViewById(INTERNAL_LIST_CONTAINER_ID); + + View rawListView = root.findViewById(INTERNAL_LIST_VIEW_ID); + if (!(rawListView instanceof RecyclerView)) { + if (rawListView == null) { + throw new RuntimeException( + "Your content must have a RecyclerView whose id attribute is " + + "'android.R.id.list'"); + } + throw new RuntimeException( + "Content has view with id attribute 'android.R.id.list' " + + "that is not a ListView class"); + } + + listView = (RecyclerView) rawListView; + } + + if(layoutManager != null) { + RecyclerView.LayoutManager manager = layoutManager; + this.layoutManager = null; + setLayoutManager(manager); + } + + listShown = true; + listView.setHasFixedSize(isFixedSize()); + listView.addOnScrollListener(scrollListener); + + if (this.adapter != null) { + A adapter = this.adapter; + this.adapter = null; + setAdapter(adapter); + } else { + // We are starting without an adapter, so assume we won't + // have our data right away and start with the progress indicator. + if (progressContainer != null) { + setListShown(false, false); + } + } + + handler.post(requestFocus); + } +} diff --git a/OpenKeychain/src/main/res/layout/key_list_dummy.xml b/OpenKeychain/src/main/res/layout/key_list_dummy.xml new file mode 100644 index 000000000..afdd88f0c --- /dev/null +++ b/OpenKeychain/src/main/res/layout/key_list_dummy.xml @@ -0,0 +1,47 @@ + + + + + + + + + + + + + + \ No newline at end of file diff --git a/OpenKeychain/src/main/res/layout/key_list_fragment.xml b/OpenKeychain/src/main/res/layout/key_list_fragment.xml index ce40dac8c..8f79d9c4e 100644 --- a/OpenKeychain/src/main/res/layout/key_list_fragment.xml +++ b/OpenKeychain/src/main/res/layout/key_list_fragment.xml @@ -1,115 +1,140 @@ - + xmlns:android="http://schemas.android.com/apk/res/android" - - + + + android:layout_height="match_parent" + android:gravity="center"> - + + + + + + + + android:layout_height="match_parent"> - + - + android:orientation="vertical" + android:animateLayoutChanges="true" > - - - - -