SectionIndexer with GridView in Android - android

Is it possible to use a SectionIndexer with a GridView in Android? Fast scroll is working fine, and I'm using a custom adapter that extends BaseAdapter. The adapter is currently implementing SectionIndexer and seems to be identical to the examples shown online and on Stack Overflow. This made me think if it's even possible to do with a GridView and a custom adapter.

static class YOUR_ADAPTER extends SimpleCursorAdapter implements SectionIndexer {
private AlphabetIndexer mIndexer;
YOUR_ADAPTER (Context context, AlbumBrowserActivity currentactivity,
int layout, Cursor cursor, String[] from, int[] to) {
super(context, layout, cursor, from, to);
getColumnIndices(cursor);
}
private void getColumnIndices(Cursor cursor) {
if (cursor != null) {
YOUR_COLUMN = cursor.getColumnIndexOrThrow(WHAT_YOU'RE_SORTING);
if (mIndexer != null) {
mIndexer.setCursor(cursor);
} else {
mIndexer = new AlphabetIndexer(cursor, YOUR_COLUMN, mResources.getString(
R.string.fast_scroll_alphabet));
}
}
}
#Override
public Object[] getSections() {
return mIndexer.getSections();
}
#Override
public int getPositionForSection(int section) {
return mIndexer.getPositionForSection(section);
}
#Override
public int getSectionForPosition(int position) {
return 0;
}
}
fast_scroll_alphabet String
<string name="fast_scroll_alphabet">\u0020ABCDEFGHIJKLMNOPQRSTUVWXYZ</string>
That's a basic example, but there's not much more to it than that. Implementing SectionIndexer is pretty simple.

Related

RecyclerView blocks UI when setting the adapter

I have a requirement in which I have a populated database with over 300k rows. I have successfully implemented a CursorAdapter based in this question, with a mix of the two most up voted answers HERE.
I have implemented an AsyncTask for background service to perform the query to the database which is very fast, doesn't take more than 2-3 seconds. My ProgressDialog from the AsyncTask is at times hard to detect.
My problem is, when the task is done and I retrieve the Cursor, when I set the Adapter to the RecyclerView, the process freezes my UI for a few seconds until the data is set. It also happens when I perform a search (new query, same procedure as getting all the rows but with fewer rows), and replace the Cursor to update the data.
Here is some relevant code:
AsyncTask
#Override
protected Void doInBackground(Void... Void) {
if(type==Constants.GET_ZIP_CODES)
cursor = db.getAllZipCodes();
else
cursor = db.searchZipCodes(text);
return null;
}
#Override
protected void onPostExecute(Void Void) {
setAdapter();
mProgressDialog.dismiss();
super.onPostExecute(Void);
}
Methods
private void setAdapter(){
if(myAdapter == null){
myAdapter = new MyAdapter(getActivity(), cursor);
search_rv.setAdapter(myAdapter);
} else
myAdapter.swapCursor(cursor);
}
Since it is a search I don't have much to do here besides notifyDataSetChanged() because all the data changes widely in every search.
Is this normal? Since a RecyclerView only renders the visible views, why does it freeze and takes so long to update since the Cursor is already ready from the AsyncTask?
EDIT
I have changed my Adapter to avoid using CursorAdapter as #cricket_007 pointed out having an Adapter within an Adapter is bad design.
This is my Adapter:
public class SearchListAdapter extends RecyclerView.Adapter<SearchListAdapter.ViewHolder> {
private Context mContext;
private Cursor mCursor;
private boolean mDataValid;
private int mRowIdColumn;
private DataSetObserver mDataSetObserver;
public SearchListAdapter(Context context, Cursor c) {
mContext = context;
mCursor=c;
mDataValid = c != null;
mRowIdColumn = mDataValid ? mCursor.getColumnIndex("_id") : -1;
mDataSetObserver = new NotifyingDataSetObserver();
if (mCursor != null) {
mCursor.registerDataSetObserver(mDataSetObserver);
}
}
static class ViewHolder extends RecyclerView.ViewHolder {
TextView itemTV;
ViewHolder(View itemView) {
super(itemView);
itemTV = (TextView) itemView.findViewById(R.id.itemTV);
}
}
#Override
public void setHasStableIds(boolean hasStableIds) {
super.setHasStableIds(true);
}
#Override
public int getItemCount() {
if (mDataValid && mCursor != null) {
return mCursor.getCount();
}
return 0;
}
#Override
public void onBindViewHolder(ViewHolder holder, int position) {
// Passing the binding operation to cursor loader
mCursor.moveToPosition(position);
String town = mCursor.getString(mCursor.getColumnIndex(Constants.COLUMN_TOWN));
String zipcode = mCursor.getString(mCursor.getColumnIndex(Constants.COLUMN_ZIPCODE));
String zipcode_etx = mCursor.getString(mCursor.getColumnIndex(Constants.COLUMN_ZIPCODE_EXTENSION));
holder.itemTV.setText(zipcode+"-"+zipcode_etx+", "+town);
}
#Override
public SearchListAdapter.ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
View itemView = LayoutInflater.from(parent.getContext()).inflate(R.layout.fragment_search_list_item,parent,false);
// Passing the inflater job to the cursor-adapter
return new SearchListAdapter.ViewHolder(itemView);
}
public void swapCursor(Cursor cursor) {
Cursor old = changeCursor(cursor);
if (old != null) {
old.close();
}
}
private Cursor changeCursor(Cursor newCursor) {
if (newCursor == mCursor) {
return null;
}
final Cursor oldCursor = mCursor;
if (oldCursor != null && mDataSetObserver != null) {
oldCursor.unregisterDataSetObserver(mDataSetObserver);
}
mCursor = newCursor;
if (mCursor != null) {
if (mDataSetObserver != null) {
mCursor.registerDataSetObserver(mDataSetObserver);
}
mRowIdColumn = newCursor.getColumnIndexOrThrow("_id");
mDataValid = true;
notifyDataSetChanged();
} else {
mRowIdColumn = -1;
mDataValid = false;
notifyDataSetChanged();
}
return oldCursor;
}
private class NotifyingDataSetObserver extends DataSetObserver {
#Override
public void onChanged() {
super.onChanged();
mDataValid = true;
notifyDataSetChanged();
}
#Override
public void onInvalidated() {
super.onInvalidated();
mDataValid = false;
notifyDataSetChanged();
}
}
}
Well, I found out why this was happening and the reason is weird. The problem has nothing to do with the RecyclerView but with the way the data is fetch.
In my AsyncTask, where I fetch the data, I wrote a Log.d to print the Cursor size like so:
#Override
protected Void doInBackground(Void... Void) {
if(type==Constants.GET_ZIP_CODES)
cursor = db.getAllZipCodes();
else
cursor = db.searchZipCodes(text);
Log.d("DATABASE","SIZE "+cursor.getCount());
return null;
}
This made the AsyncTask take longer, the ProgressDialog takes longer to go off. What I understand is that somehow, the database query is performed, the code keeps compiling, but the data is only ready in the Cursor after a while. Once I printed the result just after the query, it didn't go past the line until the cursor was fully loaded.
Actually this is not an answer(Would have put it in comment if i had enough reputation points) just a suggestion/case study i came across while loading data from database to recyclerView. Instead of directly sending the cursor over to adapter i sent it as an arraylist, but thats besides the point.
The place where i got the freeze like you seem to get is when i have to load a text with over 700-800 character into a card. So when i crop the text to less than 600 the freeze vanishes.
So just check if you have any data with large character set, if so try removing that and test it out.
Hope it works for you, suggestion put read more option for large text like whatsapp does!

Secondary sort on ArrayAdapter

i am trying to sort my Arrayadapter with it's sort-Method
#Override
public void sort(#NonNull Comparator<? super Task> comparator) {
super.sort(comparator);
}
two times.
I'm using the two Comparators
private static final Comparator TASKCOMPARATOR_TITLE = new Comparator<Task>() {
#Override
public int compare(Task a, Task b) {
return a.getTitle().compareToIgnoreCase(b.getTitle());
}
};
private static final Comparator TASKHOLDERCOMPARATOR_DUEDATE = new Comparator<ViewHolder>() {
#Override
public int compare(ViewHolder a, ViewHolder b) {
return a.getTask().getDueTime().compareTo(b.getTask().getDueTime());
}
};
like this
taskAdapter.sort(Util.getTASKCOMPARATOR_TITLE());
taskAdapter.sort(Util.getTASKCOMPARATOR_DUEDATE());
hoping to secondary sort the ArrayAdapter by the title and then by the Date. The sort-Method of the ArrayAdapter is internally using
public void sort(Comparator<? super T> comparator) {
synchronized (mLock) {
if (mOriginalValues != null) {
Collections.sort(mOriginalValues, comparator);
} else {
Collections.sort(mObjects, comparator);
}
}
if (mNotifyOnChange) notifyDataSetChanged();
}
I've read, that Collections.sort() is using a stable algorithm and therefore i'm wondering why the List of my ArrayAdapter is just being sorted by the last comparator i call.
Can anyone tell me where i made a mistake and why the first sort()-call is being ignored by the second one?
EDIT
private static final Comparator TASKCOMPARATOR = new Comparator<Task>() {
#Override
public int compare(Task a, Task b) {
int timeCompareResult = a.getDueTime().compareTo(b.getDueTime());
if (timeCompareResult == 0) {
return a.getTitle().compareToIgnoreCase(b.getTitle());
} else {
return timeCompareResult;
}
}
};
This works. I don't know if it's the only/best way.
As Шах stated, use one comparator.
Comparator returns 0 on equals.
First compare on Title, if they are equal you go for the comparison on Time.
I haven't ran the code, but it should be something like this:
private static final Comparator TASKHOLDERCOMPARATOR_DUEDATE = new Comparator<ViewHolder>() {
#Override
public int compare(ViewHolder a, ViewHolder b) {
int comparison = a.getTask().getTitle().compareToIgnoreCase(b.getTask().getTitle());
if(comparison == 0){
comparison = a.getTask().getDueTime().compareTo(b.getTask().getDueTime());
}
return comparison;
}
};

RecyclerView only updates on app startup

newbie to Android here!
I've been learning how to implement SQLite in my app, and to sum it up, I have an Accountant class which has access to the SQLite database. The class pulls up the items from the database and puts them in an ArrayList. This ArrayList is what is used for my adapter for the recyclerView.
Whenever I add a new item in the app, the the item's data is stored in the database and the Accountant class's ArrayListgets updated with this info.
Then, the adapter calls its notifyDataSetChanged() method to update the View. This is where the problem occurs; the RecyclerView DOES display all items, but only upon app startup, NOT when a new item is added.
I've done all I can, it just LOOKS like it's supposed to work, but it doesn't and it's driving me nuts.
Here's the code
ItemAdapter Class
private class ItemAdapter extends RecyclerView.Adapter<ItemHolder> {
private List<Item> mItemList;
public ItemAdapter(List<Item> itemList) {
mItemList = itemList;
}
public ItemHolder onCreateViewHolder(ViewGroup parent, int ViewType) {
View view = getLayoutInflater().inflate(R.layout.list_item_item, parent, false);
return new ItemHolder(view);
}
public void onBindViewHolder(ItemHolder holder, int position) {
Item item = mItemList.get(position);
holder.bindItem(item);
}
public int getItemCount() {
return mItemList.size();
}
}
Accountant Class
public class Accountant {
private static Accountant sAccountant;
private double mTotalMoney;
private Context mContext;
private SQLiteDatabase mDatabase;
private List<Item> mItemList;
public static Accountant get(Context context) {
sAccountant = sAccountant == null ? new Accountant(context) : sAccountant;
return sAccountant;
}
private Accountant(Context context) {
mTotalMoney = 0;
mContext = context.getApplicationContext();
mDatabase = new ItemBaseHelper(mContext).getWritableDatabase();
mItemList = getListFromSQL();
}
private static ContentValues getContentValues(Item i) {
ContentValues values = new ContentValues();
values.put(ItemTable.cols.NAME, i.getName());
values.put(ItemTable.cols.PRICE, i.getPrice());
values.put(ItemTable.cols.COUNT, i.getCount());
return values;
}
public void addItem(Item item) {
ContentValues cv = getContentValues(item);
mDatabase.insert(ItemTable.NAME, null, cv);
mItemList = getListFromSQL();
}
public void removeItem(int i) {
}
public void addMoney(double money, boolean isSet) {
mTotalMoney += isSet ? money - mTotalMoney : money;
}
public String getTotalMoney() {
return MoneyUtils.prep(mTotalMoney);
}
public String getChange() {
double cost = 0;
for (Item item : getItemList())
cost += item.getPrice() * item.getCount();
return MoneyUtils.prep(mTotalMoney - cost);
}
public List<Item> getItemList() {
return mItemList;
}
private List<Item> getListFromSQL() {
List<Item> itemList = new ArrayList<>();
ItemCursorWrapper cursor = queryItems(null, null);
try {
cursor.moveToFirst();
while (!cursor.isAfterLast()) {
itemList.add(cursor.getItem());
cursor.moveToNext();
}
} finally {
cursor.close();
}
return itemList;
}
public ItemCursorWrapper queryItems(String whereClause, String[] whereArgs) {
Cursor cursor = mDatabase.query(ItemTable.NAME, null, whereClause, whereArgs, null, null, null);
return new ItemCursorWrapper(cursor);
}
public String individualPriceOf(Item i) {
return MoneyUtils.prep(i.getPrice());
}
public String totalPriceOf(Item i) {
return MoneyUtils.prep(i.getCount() * i.getPrice());
}
public String countOf(Item i) {
return String.valueOf(i.getCount());
}
public void clearList() {
mDatabase.delete(ItemTable.NAME, null, null);
}
}
Item adding logic
public void addItem(Item item) {
mAccountant.addItem(item);
mAdapter.notifyItemInserted(mAccountant.getListFromSQL().size() - 1);
mAdapter.notifyDataSetChanged();
mChangeButton.setText(mAccountant.getChange());
}
Well there is fundamental problem not even related to RecyclerView.
First let's see how to fix your issue then explanation of what's wrong.
change this
private List<Item> mItemList;
to this
private final List<Item> mItemList;
then instead of any assignment like mItemList = getListFromSQL(); write this
mItemList.clear();
mItemList.addAll(getListFromSQL());
Now explanation why your code is not working. The thing is that when you assign your dataSource (i.e. mItemList) to some new value you are changing reference to it (that's a java fundamental thing) so that your RecyclerView doesn't know anything about it and it's own dataSource which you assign only once in constructor remains the same old one which is not changed therefore your notifyDataSetChanged call does nothing.
General advice whenever using RecyclerView or a ListView make sure you define your dataSource as final.
This is happening because you do not add the item into your Adpater's list. Make a method inside your adapter and call this method from your Accountant class.
private class ItemAdapter extends RecyclerView.Adapter<ItemHolder> {
public void addItem(Item item) {
mItemList.add(item); ///Add the item to your arrayList and then notify
notifyItemInserted(mItemList.size());
}
When you add single item in Adapter dont call notifyDataSetChanged() method because it will notify the whole list. Instead only use notifyItemInserted() method.
Another think is make sure when you notify the adapter it must be from UI thread.
When you add your item then just call this adapter addItem() method from your Accountant class.
public void addItem(Item item) { ///This method is from Accountant Class
mAccountant.addItem(item);
mAdapter.addItem(item); // Call the addItem() from Adapter class
mChangeButton.setText(mAccountant.getChange());
}
Hope it will work...

SectionIndexer with AlphabetIndexer stuck at K

My listfragment has a custom CursorAdapter that implements the SectionIndexer. I use the AlphabetIndexer helper class to implement the SectionIndexer in a simple standard way. But for some reason, while scrolling, the character in the scroll changes from K to R, skipping all the letters in between. So even though there are dozens of entries starting with L,M,N,O,P,Q, they all are coming under the section K. If i remove the K from the alphabet parameter of the AlphabetIndexer, the same behaviour persist with L. Any idea why this could happen?
private class ContactsAdapter extends CursorAdapter implements SectionIndexer {
private AlphabetIndexer mAlphabetIndexer; // Stores the AlphabetIndexer instance
...
public ContactsAdapter(Context context) {
...
final String alphabet = context.getString(R.string.alphabet); // alphabet=" ABCDEFGHIJKLMNOPQRSTUVWXYZ"
mAlphabetIndexer = new AlphabetIndexer(null, ContactsQuery.SORT_KEY, alphabet);
/**
* An override of getCount that simplifies accessing the Cursor. If the Cursor is null,
* getCount returns zero. As a result, no test for Cursor == null is needed.
*/
#Override
public int getCount() {
if (getCursor() == null) {
return 0;
}
return super.getCount();
}
#Override
public Cursor swapCursor(Cursor newCursor) {
// Update the AlphabetIndexer with new cursor as well
mAlphabetIndexer.setCursor(newCursor);
return super.swapCursor(newCursor);
}
#Override
public Object[] getSections() {
return mAlphabetIndexer.getSections();
}
/**
* Defines the SectionIndexer.getPositionForSection() interface.
*/
#Override
public int getPositionForSection(int i) {
if (getCursor() == null) {
return 0;
}
return mAlphabetIndexer.getPositionForSection(i);
}
/**
* Defines the SectionIndexer.getSectionForPosition() interface.
*/
#Override
public int getSectionForPosition(int i) {
if (getCursor() == null) {
return 0;
}
return mAlphabetIndexer.getSectionForPosition(i);
}
}
}

Unable to make AlphabetIndexer work

I'm attempting to implement the AlphabetIndexer to help the users scroll through my list, but nothing shows up on the list when I run the app. Could someone please tell me why?
Note: I am not instantiating an AlphabetIndexer in the Adapter's constructor because, at that point, no Cursor is available.
Here is the relevant code:
In the Activity's onCreate() method:
mList = (ListView)findViewById(R.id.mylist);
mList.setOnItemClickListener(this);
mList.setFastScrollEnabled(true);
mAdapter = new MyAdapter(MyActivity.this, R.layout.layout_list_row, null, new String[] {MyColumns.NAME}, new int[] {R.id.itemname});
mList.setAdapter(mAdapter);
mList.setFastScrollEnabled(true);
doQuery();
doQuery() is a method that queries for a Cursor using an AsyncQueryHandler. The AsyncQueryHandler looks like this:
private final class MyQueryHandler extends AsyncQueryHandler {
public MyQueryHandler(Context context) {
super(context.getContentResolver());
}
#Override
protected void onQueryComplete(int token, Object cookie, Cursor cursor) {
if (!isFinishing()) {
if (mAdapter != null) {
mAdapter.changeCursor(cursor);
}
}
else {
cursor.close();
}
}
}
Lastly, my SimpleCursorAdapter. I've taken out the unnecessary parts:
public class MyAdapter extends SimpleCursorAdapter implements View.OnClickListener {
private Cursor mCursor;
AlphabetIndexer alphaIndexer;
public MyAdapter(Context context, int layout, Cursor c, String[] from, int[] to) {
super(context, layout, c, from, to);
}
public int getPositionForSection(int section) {
return alphaIndexer.getPositionForSection(section);
}
public int getSectionForPosition(int position) {
return alphaIndexer.getSectionForPosition(position);
}
public Object[] getSections() {
return alphaIndexer.getSections();
}
public void onClick(View v) {
// ...
}
#Override
public void bindView(View view, Context context, Cursor cursor) {
// ...
}
#Override
public View newView(Context context, Cursor cursor, ViewGroup parent) {
// ...
}
#Override
public void changeCursor(Cursor cursor) {
super.changeCursor(cursor);
if (MyActivity.this.mCursor != null) {
stopManagingCursor(MyActivity.this.mCursor);
MyActivity.this.mCursor.close();
MyActivity.this.mCursor = null;
mCursor = null;
}
MyActivity.this.mCursor = cursor;
startManagingCursor(MyActivity.this.mCursor);
mCursor = cursor;
alphaIndexer = new AlphabetIndexer(mCursor, mCursor.getColumnIndex(MyColumns.NAME), " ABCDEFGHIJKLMNOPQRSTUVWXYZ");
alphaIndexer.setCursor(mCursor);
}
#Override
public Cursor runQueryOnBackgroundThread(CharSequence constraint) {
return doQuery();
}
}
Sometimes Android will hide the fast-scroll functionality if your list isn't long enough to warrant fast-scrolling. Not sure if that's your problem, but it might be worth trying to add a bunch of items to the list.
I've just lost couple of hours on alphabet indexer and fast scroller. In my case the list wasn't always long enough to warant the fast scroll/alphabet indexer feature. The exact behavior can be found in class FastScroller which is a helper class for AbsListView. There is a piece of code there that decides if "the list is long"
final boolean longList = childCount > 0 && itemCount / childCount >= MIN_PAGES;
MIN_PAGES is defined with value of 4. There you have it, if your list item count is not at least 4x the child count (visible rows) fast scroller and thus alphabet indexer will not appear.

Categories

Resources