I have followed a few stackoverflow threads, tutorials and what I can gather from the documentation but just can't get the AplhabetIndexer working in Android. The goal is to have an indexed ListView that users can quickly scroll using the letters on the right as per the standard contacts app on your phone. Eventually I'll add section headers in the list and make it filterable as a user types but for now I just want to get the basic list working.
I can load the list and get all my results from the cursor, but I never get the letters appear on the right of the ListView. I've tried different combinations of setting the adapter, including in the onCreateView with a null cursor and then calling changeCursor(cursor) in the onLoadFinished() callback, as well as the current version below which sets up the adapter completely in the onLoadFinished() callback.
Has anyone got a full working version of their setup and adapter code they could share? Preferably using the method of creating the adapter first, then just calling changeCursor(cursor) in the onLoadFinished() callback.
What I have so far:
StoreListAdapter.java
public class StoreListAdapter extends SimpleCursorAdapter implements SectionIndexer {
private AlphabetIndexer mAlphabetIndexer;
public StoreListAdapter(Context context, int layout, Cursor cursor, String[] from, int[] to, int flags) {
super(context, layout, cursor, from, to, flags);
if(cursor != null){
mAlphabetIndexer = new AlphabetIndexer(cursor,
cursor.getColumnIndex(StoreEntry.TABLE_ALIAS + StoreEntry.COLUMN_NAME),
"ABCDEFGHIJKLMNOPQRTSUVWXYZ");
mAlphabetIndexer.setCursor(cursor);
}
}
#Override
public void changeCursor(Cursor cursor) {
super.changeCursor(cursor);
if(cursor != null){
mAlphabetIndexer = new AlphabetIndexer(cursor,
cursor.getColumnIndex(StoreEntry.TABLE_ALIAS + StoreEntry.COLUMN_NAME),
"ABCDEFGHIJKLMNOPQRTSUVWXYZ");
mAlphabetIndexer.setCursor(cursor);
}
}
#Override
public Object[] getSections() {
if(mAlphabetIndexer != null){
return mAlphabetIndexer.getSections();
}else{
return null;
}
}
#Override
public int getPositionForSection(int sectionIndex) {
if(mAlphabetIndexer != null){
return mAlphabetIndexer.getPositionForSection(sectionIndex);
}else{
return 0;
}
}
#Override
public int getSectionForPosition(int position) {
if(mAlphabetIndexer != null){
return mAlphabetIndexer.getSectionForPosition(position);
}else{
return 0;
}
}
}
StoreListFragment.java
public class StoreListFragment extends Fragment implements LoaderManager.LoaderCallbacks<Cursor> {
private ListView mListView;
private StoreListAdapter mAdapter;
public static StoreListFragment newInstance() {
StoreListFragment fragment = new StoreListFragment();
return fragment;
}
/**
* Mandatory empty constructor for the fragment manager to instantiate the
* fragment (e.g. upon screen orientation changes).
*/
public StoreListFragment() {
}
#Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
getLoaderManager().initLoader(0, null, this);
}
#Nullable
#Override
public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
View view = inflater.inflate(R.layout.fragment_store_search, container, false);
mListView = (ListView) view.findViewById(R.id.search_result_list);
return view;
}
#Override
public void onAttach(Activity activity) {
super.onAttach(activity);
}
#Override
public void onDetach() {
super.onDetach();
}
#Override
public Loader<Cursor> onCreateLoader(int id, Bundle args) {
return new CursorLoader(
getActivity(), // Parent activity context
StoreProvider.CONTENT_URI, // Table to query
null, // Projection to return
null, // No selection clause
new String[]{getString(R.string.centre_id)}, // No selection arguments
null // Default sort order
);
}
#Override
public void onLoadFinished(Loader<Cursor> loader, Cursor data) {
mListView.setFastScrollEnabled(true);
mListView.setScrollingCacheEnabled(true);
mAdapter = new StoreListAdapter(getActivity().getApplicationContext(), R.layout.store_list_item, data, new
String[]{StoreEntry.TABLE_ALIAS + StoreEntry.COLUMN_NAME}, new int[]{R.id.item_name}, CursorAdapter.FLAG_REGISTER_CONTENT_OBSERVER);
mListView.setAdapter(mAdapter);
}
#Override
public void onLoaderReset(Loader<Cursor> loader) {
mAdapter.changeCursor(null);
}
}
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.
Actually, when I added more test data it started working. Would appear that if your search results are small (I was using about 20 or so) it doesn't kick in. Once I added some dummy data of about 100 or so then it started working.
Related
I have tried loading the list using the ListView along with LoaderManager.LoaderCallbacks and custom CursorAdapter and it works fine. But I am trying to accomplish the same using RecyclerView along with custom RecyclerView.Adapter but I am getting this issue:
I am getting the list displayed for the first time but when I rotate the device the list disappears.
Here is the code, please have a look.
CatalogActivity
public class CatalogActivity extends AppCompatActivity implements ItemAdapter.OnItemClickListener,
LoaderManager.LoaderCallbacks<Cursor> {
private static final int ITEMS_LOADER_ID = 1;
public static final String EXTRA_ITEM_NAME = "extra_item_name";
public static final String EXTRA_ITEM_STOCK = "extra_item_stock";
#BindView(R.id.list_items)
RecyclerView mListItems;
private ItemAdapter mItemAdapter;
#Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_catalog);
ButterKnife.bind(this);
setupListItems();
getLoaderManager().initLoader(ITEMS_LOADER_ID, null, this);
}
private void setupListItems() {
mListItems.setHasFixedSize(true);
LayoutManager layoutManager = new LinearLayoutManager(this);
mListItems.setLayoutManager(layoutManager);
mListItems.setItemAnimator(new DefaultItemAnimator());
mListItems.addItemDecoration(new DividerItemDecoration(this, LinearLayout.VERTICAL));
mItemAdapter = new ItemAdapter(getApplicationContext(), this);
mListItems.setAdapter(mItemAdapter);
}
#Override
public void OnClickItem(int position) {
Intent intent = new Intent(this, EditorActivity.class);
Item item = mItemAdapter.getItems().get(position);
intent.putExtra(EXTRA_ITEM_NAME, item.getName());
intent.putExtra(EXTRA_ITEM_STOCK, item.getStock());
startActivity(intent);
}
private ArrayList<Item> getItems(Cursor cursor) {
ArrayList<Item> items = new ArrayList<>();
if (cursor != null) {
while (cursor.moveToNext()) {
int columnIndexId = cursor.getColumnIndex(ItemEntry._ID);
int columnIndexName = cursor.getColumnIndex(ItemEntry.COLUMN_NAME);
int columnIndexStock = cursor.getColumnIndex(ItemEntry.COLUMN_STOCK);
int id = cursor.getInt(columnIndexId);
String name = cursor.getString(columnIndexName);
int stock = Integer.parseInt(cursor.getString(columnIndexStock));
items.add(new Item(id, name, stock));
}
}
return items;
}
#Override
public Loader<Cursor> onCreateLoader(int loaderId, Bundle bundle) {
switch (loaderId) {
case ITEMS_LOADER_ID: {
String[] projection = {
ItemEntry._ID,
ItemEntry.COLUMN_NAME,
ItemEntry.COLUMN_STOCK
};
return new CursorLoader(
this,
ItemEntry.CONTENT_URI,
projection,
null,
null,
null
);
}
default:
return null;
}
}
#Override
public void onLoadFinished(Loader<Cursor> loader, Cursor cursor) {
mItemAdapter.setItems(getItems(cursor));
}
#Override
public void onLoaderReset(Loader<Cursor> loader) {
}
}
ItemAdapter
public class ItemAdapter extends RecyclerView.Adapter<ItemAdapter.ItemViewHolder> {
private ArrayList<Item> mItems;
private OnItemClickListener mOnItemClickListener;
private Context mContext;
public ItemAdapter(Context context, OnItemClickListener onItemClickListener) {
mOnItemClickListener = onItemClickListener;
mContext = context;
}
public void setItems(ArrayList<Item> items) {
if (items != null) {
mItems = items;
notifyDataSetChanged();
}
}
public ArrayList<Item> getItems() {
return mItems;
}
public interface OnItemClickListener {
void OnClickItem(int position);
}
public class ItemViewHolder extends RecyclerView.ViewHolder implements View.OnClickListener {
#BindView(R.id.tv_item)
TextView tv_item;
#BindView(R.id.tv_stock)
TextView tv_stock;
public ItemViewHolder(#NonNull View itemView) {
super(itemView);
ButterKnife.bind(this, itemView);
itemView.setOnClickListener(this);
}
#Override
public void onClick(View view) {
int position = getAdapterPosition();
mOnItemClickListener.OnClickItem(position);
}
}
#NonNull
#Override
public ItemViewHolder onCreateViewHolder(#NonNull ViewGroup parent, int i) {
View itemView = LayoutInflater.from(parent.getContext())
.inflate(R.layout.item_inventory, parent, false);
return new ItemViewHolder(itemView);
}
#Override
public void onBindViewHolder(#NonNull ItemViewHolder itemViewHolder, int position) {
final Item item = mItems.get(position);
itemViewHolder.tv_item.setText(item.getName());
itemViewHolder.tv_stock.setText(mContext.getString(R.string.display_stock, item.getStock()));
}
#Override
public int getItemCount() {
if (mItems == null) {
return 0;
} else {
return mItems.size();
}
}
}
I am not able to figure out the extact issue. Please help.
Briefly, the issue here is that, after rotation, you're being handed the same Cursor that you had previously looped over before the rotation, but you're not accounting for its current position.
A Cursor tracks and maintains its own position within its set of records, as I'm sure you've gathered from the various move*() methods it contains. When first created, a Cursor's position will be set to right before the first record; i.e., its position will be set to -1.
When you first start your app, the LoaderManager calls onCreateLoader(), where your CursorLoader is instantiated, and then causes it to load and deliver its Cursor, with the Cursor's position at -1. At this point, the while (cursor.moveToNext()) loop works just as expected, since the first moveToNext() call will move it to the first position (index 0), and then to each available position after that, until the end.
Upon rotation, however, the LoaderManager determines that it already has the requested Loader (determined by ID), which itself sees that it already has the appropriate Cursor loaded, so it just immediately delivers that same Cursor object again. (This is a major feature of the Loader framework – it won't reload resources it already has, regardless of configuration changes.) This is the crux of the issue. That Cursor has been left at the last position to which it was moved before the rotation; i.e., at its end. Consequently, the Cursor cannot moveToNext(), so that while loop just never runs at all, after the initial
onLoadFinished(), before rotation.
The simplest fix, with the given setup, would be to manually reposition the Cursor yourself. For example, in getItems(), change the if to moveToFirst() if the Cursor is not null, and change the while to a do-while, so we don't inadvertently skip over the first record. That is:
if (cursor != null && cursor.moveToFirst()) {
do {
int columnIndexId = cursor.getColumnIndex(ItemEntry._ID);
...
} while (cursor.moveToNext());
}
With this, when that same Cursor object is re-delivered, its position is kinda "reset" to position 0. Since that position is directly on the first record, rather than right before it (remember, initially -1), we change to a do-while, so that the first moveToNext() call doesn't skip the first record in the Cursor.
Notes:
I would mention that it is possible to implement a RecyclerView.Adapter to take a Cursor directly, similar to the old CursorAdapter. In this, the Cursor would necessarily be moved in the onBindViewHolder() method to the correct position for each item, and the separate ArrayList would be unnecessary. It'd take a little effort, but translating CursorAdapter to a RecyclerView.Adapter isn't terribly difficult. Alternatively, there are certainly solutions already available. (For example, possibly, this one, though I cannot vouch for it, atm, I often see a trusted fellow user recommend it often.)
I would also mention that the native Loader framework has been deprecated, in favor of the newer ViewModel/LiveData architecture framework in support libraries. However, it appears that the newest androidx library has its own internal, improved Loader framework which is a simple wrapper around said ViewModel/LiveData setup. This seems to be a nice, easy way to utilize the known Loader constructs while still benefiting from the recent architecture refinements.
Instead of LoaderManager.initLoader() call LoaderManager.restartLoader()
I have a FragmentStatePagerAdapter with a Listview in each page. The FragmentStatePagerAdapter is being refreshed once every second with new data for some of his pages (remember that each page is a listview).
I set the content of the listview with setAdapter:
StableArrayAdapter adapter = new StableArrayAdapter((MainActivity) getActivity(), R.layout.list_view_item, ((MainActivity) getActivity()).getData(mPage));
listView.setAdapter(adapter);
The problem is that some pages has a lot of content and a vertical scroll, so every second when listView.setAdapter(adapter); is being called, the scroll of the listview is forced to his upper position. It is a very abnormal and annoying behaviour.
I find this on Stack Overflow: notifyDataSetChanged() makes the list refresh and scroll jumps back to the top
The problem is that these solutions are designed for a normal ViewPager or normal Adapter and can't work with FragmentStatePageAdapter or something, because after trying them I still have the same problem.
This is my adapter for the FragmentStatePagerAdapter:
public class CollectionPagerAdapter extends FragmentStatePagerAdapter {
public CollectionPagerAdapter(FragmentManager fm) {
super(fm);
}
#Override
public Fragment getItem(int position) {
return ObjectFragment.newInstance(position);
}
#Override
public int getCount() {
return infoTitlesArray.length;
}
#Override
public CharSequence getPageTitle(int position) {
return infoTitlesArray[position];
}
}
The ViewPager which has the problem:
<android.support.v4.view.ViewPager
xmlns:android="http://schemas.android.com/apk/res/android"
android:id="#+id/pager"
android:layout_width="match_parent"
android:layout_height="match_parent">
<android.support.v4.view.PagerTitleStrip
android:id="#+id/pager_title_strip"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_gravity="top"
android:paddingTop="4dp"
android:paddingBottom="4dp"
style="#style/CustomPagerTitleStrip"/>
</android.support.v4.view.ViewPager>
Layout of the fragment:
<ListView xmlns:android="http://schemas.android.com/apk/res/android"
android:id="#+id/listview"
android:layout_width="wrap_content"
android:layout_height="wrap_content">
</ListView>
The Java code for the fragment:
public static class ObjectFragment extends Fragment implements DataUpdateListener {
private int mPage;
private ListView listView;
public static ObjectFragment newInstance(int page) {
ObjectFragment objectFragment = new ObjectFragment();
Bundle args = new Bundle();
args.putInt("page", page);
objectFragment.setArguments(args);
return objectFragment;
}
#Override
public void onCreate(#Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
mPage = getArguments().getInt("page");
}
#Override
public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
View rootView = inflater.inflate(R.layout.fragment_collection_object, container, false);
((MainActivity) getActivity()).addDataUpdateListener(this);
listView = (ListView) rootView;
StableArrayAdapter adapter = new StableArrayAdapter((MainActivity) getActivity(), R.layout.list_view_item, ((MainActivity) getActivity()).getData(mPage));
listView.setAdapter(adapter);
return rootView;
}
#Override
public void onDestroyView() {
((MainActivity) getActivity()).removeDataUpdateListener(this);
super.onDestroyView();
}
//DataUpdateListener interface methods
#Override
public int getPage() {
return mPage;
}
#Override
public void onDataUpdated(ArrayList<String> data) {
StableArrayAdapter adapter = new StableArrayAdapter((MainActivity) getActivity(), R.layout.list_view_item, data);
listView.setAdapter(adapter);
}
}
When the content of the fragment (which is a listview) needs to be updated, the method onDataUpdated is called with the new content received in the parameter.
Edit
Adding adapter which generates a crash, using the code samples from Budius:
private static class StableArrayAdapter extends ArrayAdapter<String> {
HashMap<String, Integer> mIdMap = new HashMap<String, Integer>();
public StableArrayAdapter(Context context, int textViewResourceId, List<String> objects) {
super(context, textViewResourceId, objects);
for (int i = 0; i < objects.size(); ++i) {
mIdMap.put(objects.get(i), i);
}
}
public void setData(List<String> objects){
mIdMap.clear();
for (int i = 0; i < objects.size(); ++i) {
mIdMap.put(objects.get(i), i);
}
}
#Override
public long getItemId(int position) {
String item = getItem(position);
return mIdMap.get(item);
}
#Override
public boolean hasStableIds() {
return true;
}
}
Edit 2
New adapter trying to use Budius solution. But it does not work. The content is not being refreshed.
private static class StableArrayAdapter extends ArrayAdapter<String> {
List<String> objects;
public StableArrayAdapter(Context context, int textViewResourceId, List<String> objects) {
super(context, textViewResourceId, objects);
this.objects = new ArrayList<>();
this.objects.addAll(objects);
}
public void setData(List<String> objects){
this.objects.clear();
this.objects.addAll(objects);
}
#Override
public long getItemId(int position) {
return Math.abs(objects.get(position).hashCode());
}
#Override
public boolean hasStableIds() {
return true;
}
}
calling setAdapter(adapter) and the list returning to the top is normal and a "expected" behavior. Afterall, it's a new adapter and the framework should not assume that the new adapter have any relation to the old one.
Said that, your solution should still be around notifyDataSetChanged() and the appropriate usage of stableIds. With that in mind, what you should do is simply switch the ArrayList<String> data instead of creating a new one. Something like this:
#Override
public void onDataUpdated(ArrayList<String> data) {
adapter.setData(data); // internally replaces the old data with this new one
adapter.notifyDataSetChanged();
}
and then on your implementation of StableArrayAdapter you must override hasStableIds to return true and override getItemId to return a meaningfull value. Considering your data are simply strings, it's a bit tricky but something like Math.abs(value.hashCode) should be enough.
PS.: I stronly recommend using RecyclerView instead of ListView. Personally I don't understand why Google didn't deprecated ListView yet.
edit:
as per comment, sometihng like this:
public class StableArrayAdapter extends /* whatever you're extending */ {
... your code as usual
#Override public boolean hasStableIds() { return true; }
#Override public long getItemId(int position) {
return Math.abs(data.get(position).hashCode());
}
}
with that you're telling the system that the ID is direct related to the data you got and you returning ID that is connected to your data (even if losely connected). Meaning, it will be one ID for one data and a different ID for a different data.
That way on notify the ListView knows to keep te position based on the ID.
edit 2:
I found your error. Your adapter extends from ArrayAdapter. This already have a List<String> internally and when you call getItem(position);, the data comes from that internal List.
The fix is simple.
Delete the List<String> objects; and use the internal List.
To update the data you call add, addAll and clear methods from ArrayAdapter (https://developer.android.com/reference/android/widget/ArrayAdapter.html):
something like:
#Override
public void onDataUpdated(ArrayList<String> data) {
adapter.clear();
adapter.addAll(data);
adapter.notifyDataSetChanged();
}
My app uses Fragments in association with a ViewPager to create tabs.
On one tab, I use a ListView to display contents of my database in a table. I have extended SimpleCursorAdapter in order to perform some custom tasks on each row of my list (e.g., alternate row color, buttons, etc.). At one point, I want to click an icon within a row and have it switch tabs, but I am having issues accessing the FragmentManager and ViewPager from within my CustomSimpleCursorAdapter class. I can do this within the fragment itself (see the commented-out section), but when I switched to using the CustomSimpleCursorAdapter, I lost that ability.
So, how can I access my FragmentManager and ViewPager from within my custom class?
note: I have marked the location within CustomSimpleCursorAdapter.java where I am having trouble with
// THIS IS WHERE I AM HAVING ISSUES WITH:
Home.java
package myPackage;
public class Home extends Fragment {
private View rootView;
private CustomSimpleCursorAdapter mySimpleCursorAdapter;
private ViewPager myViewPager;
private SwipeRefreshLayout studentSwipeRefresh;
#Override
public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState){
rootView = inflater.inflate(R.layout.home, container, false);
myViewPager = (ViewPager) getActivity().findViewById(R.id.pager);
studentSwipeRefresh = (SwipeRefreshLayout) rootView.findViewById(R.id.student_swipe_refresh);
return rootView;
}
#Override
public void onViewCreated(View rootView, Bundle savedInstanceState) {
super.onViewCreated(rootView, savedInstanceState);
drawTheStudentView();
studentSwipeRefresh.setColorSchemeColors(Color.parseColor(Constants.RED), Color.parseColor(Constants.ORANGE), Color.parseColor(Constants.YELLOW), Color.parseColor(Constants.GREEN), Color.parseColor(Constants.BLUE), Color.parseColor(Constants.INDIGO), Color.parseColor(Constants.VIOLET));
studentSwipeRefresh.setOnRefreshListener(new SwipeRefreshLayout.OnRefreshListener() {
#Override
public void onRefresh() {
studentSwipeRefresh.setRefreshing(false);
drawTheStudentView();
}
});
}
private void drawTheStudentView(){
DatabaseHelper myDBHelper = new DatabaseHelper(getActivity());
Cursor studentCursor = myDBHelper.getStudentsCursor();
String[] fromColumns = {"_id","studentID","location"};
int[] toViews = {R.id.student_number_textview, R.id.student_id_textview, R.id.student_location.textview};
mySimpleCursorAdapter = new CustomSimpleCursorAdapter(getActivity(), R.layout.student_layout, studentCursor, fromColumns, toViews, 0);
// Replace the _id column with a student count
mySimpleCursorAdapter.setViewBinder(new SimpleCursorAdapter.ViewBinder() {
#Override
public boolean setViewValue(View view, Cursor cursor, int columnIndex) {
String counter = Integer.toString((cursor.getPosition()+1));
TextView modifiedTextView = (TextView) view;
if(columnIndex == 0){
modifiedTextView.setText(counter);
return true;
}
return false;
}
});
ListView myListView = (ListView) rootView.findViewById(R.id.student_row);
// I COMMENTED THIS OUT. THIS WORKS FOR CLICKING THE ENTIRE ROW AND HAVING AN ACTION PERFORMED.
// Listen for somebody clicking on a Student ID, and process
// myListView.setOnItemClickListener(new AdapterView.OnItemClickListener() {
// #Override
// public void onItemClick(AdapterView<?> parent, View view, int position, long id) {
// Cursor subCursor = (Cursor) mySimpleCursorAdapter.getItem(position);
// String studentIDNumber = subCursor.getString(subCursor.getColumnIndex("studentID"));
//
// StudentInformation studentInformation = (StudentInformation) getFragmentManager().findFragmentByTag(getFragmentTag(Constants.TAB_INDEX_PATIENT_VITALS));
// studentInformation.setStudendIDNumber(studentIDNumber);
//
// myViewPager.setCurrentItem(Constants.TAB_INDEX_STUDENT_LOCATION);
// }
// });
// Draw the list
myListView.setAdapter(mySimpleCursorAdapter);
myDBHelper.close();
}
// Pass me a tab index (see Constants.java) and I'll return a refrence to that tab.
private String getFragmentTag(int tagID){
return "android:switcher:" + R.id.pager + ":" + tagID;
}
}
CustomSimpleCursorAdapter.java
package myPackage;
public class CustomSimpleCursorAdapter extends SimpleCursorAdapter {
private Context context;
private Cursor cursor;
public CustomSimpleCursorAdapter(Context context, int layout, Cursor cursor, String[] from, int[] to, int flags) {
super(context, layout, cursor, from, to, flags);
this.context = context;
this.cursor = cursor;
}
#Override
public View getView(final int position, View convertView, ViewGroup parent){
// Alternate the color of the data rows.
View row = super.getView(position, convertView, parent);
if(position % 2 == 0){
row.setBackgroundColor(Color.parseColor(Constants.WHITE));
} else {
row.setBackgroundColor(Color.parseColor(Constants.LIGHTGREY));
}
// If the "Information" icon/button is clicked, set the information and switch to that tab/fragment.
ImageView statusButton = (ImageView)row.findViewById(R.id.student_status_button);
statusButton.setOnClickListener(new View.OnClickListener() {
#Override
public void onClick(View myView) {
cursor.moveToPosition(position);
String studentID = cursor.getString(1);
Toast statusToast = Toast.makeText(context, "Showing information for student " + studentID, Toast.LENGTH_SHORT);
statusToast.show();
// THIS IS WHERE I AM HAVING ISSUES WITH:
// * getFragmentManager()
// * myViewPager.setCurrentItem()
StudentInformation studentInformation = (StudentInformation) getFragmentManager().findFragmentByTag(getFragmentTag(Constants.TAB_INDEX_STUDENT_INFORMATION));
studentInformation.setStudentNumber(studentID);
myViewPager.setCurrentItem(Constants.TAB_INDEX_STUDENT_INFORMATION);
}
});
// If the "Location" button is clicked, then show a Toast with the information.
ImageView locationButton = (ImageView)row.findViewById(R.id.student_location_button);
locationButton.setOnClickListener(new View.OnClickListener() {
#Override
public void onClick(View myView) {
cursor.moveToPosition(position);
String studentID = cursor.getString(1);
Toast statusToast = Toast.makeText(context, "Sending location for student " + studentID + " to MESA", Toast.LENGTH_LONG);
statusToast.show();
}
});
return row;
}
// Pass me a tab index (see Constants.java) and I'll return a refrence to that tab.
private String getFragmentTag(int tagID){
return "android:switcher:" + R.id.pager + ":" + tagID;
}
}
UPDATE:
I tried passing in FragmentManager and ViewPager references into the constructor of my CustomSimpleCursorAdapter, but that didn't seem to work.
1) Create an interface for example:
public interface MyTabChanger{
public void changeTabTo(int nextTabIndex);
}
2) implement that interface with your fragment.
3) inside changeTabTo call in your fragment you can use below code:
myViewPager.setCurrentItem(nextTabIndex);
4) change the constructor of your Adapter
...
public class CustomSimpleCursorAdapter extends SimpleCursorAdapter {
private Context context;
private Cursor cursor;
private MyTabChanger mCallback;
public CustomSimpleCursorAdapter(MyTabChanger myTabChanger,Context context, int layout, Cursor cursor, String[] from, int[] to, int flags) {
super(context, layout, cursor, from, to, flags);
this.context = context;
this.cursor = cursor;
mCallback = myTabChanger;
}
...
5) then in your adapter when you want to change the viewpager simply call changeTabTo. For example:
mCallback.changeTabTo(Constants.TAB_INDEX_STUDENT_INFORMATION);
I think this is the general and cleanest solution you can use. In this way you can use the adapter with any Fragment and Activity as long as they implements MyTabChanger.
You can use cast the Context you initialize the adapter with to your activity (provided you use the activity as the context)
public CustomSimpleCursorAdapter(Context context, int layout, Cursor cursor, String[] from, int[] to, int flags) {
super(context, layout, cursor, from, to, flags);
this.context = context;
this.cursor = cursor;
//Where you need to...
((MyActivity)context).mViewPager.setCurrentItem()
}
Hopefully this gives you a good idea of what you need to do :)
I'm new to android.
I'm trying to create a recyclerAdapter in my app with loaderManager to load cursor from sqlite asynchronously.
When there is new data available from sqlite database, I want to insert new items on top of recyclerView. But at the same time, also want to maintain current viewing item.
Let's say, I'm currently viewing item range 5th to 10th, when new 10 items come in, current viewing position also should be 15th to 20th (not to new 5th to 10th).
Otherwise, if lots of items (50 to 60 items) insert, user will lose what he was previously looking at.
So, Is there any way that can maintain current viewing item with recyclerView and loaderManager? How Can it be done simply?
Edit
My code are still simple. Nothing complicated yet to mention specially. But if u insist, Here is my codes.
MyActivity.java
public class MyActivity extends BaseActivity
implements LoaderManager.LoaderCallbacks<Cursor>{
private CustomAdapter mAdapter;
#Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_list_layout);
mAdapter = new CustomAdapter();
RecyclerView recycler = (RecyclerView) findViewById(R.id.recycler_view);
recycler.setAdapter(mAdapter);
recycler.getItemAnimator().setAddDuration(1000);
recycler.getItemAnimator().setChangeDuration(1000);
recycler.setHasFixedSize(true);
LinearLayoutManager llm = new LinearLayoutManager(this);
llm.setOrientation(LinearLayoutManager.VERTICAL);
recycler.setLayoutManager(llm);
getLoaderManager().initLoader(0, null, this);
}
#Override
protected void onDestroy() {
if(mAdapter != null || mAdapter.getItemCount() != 0){
mAdapter.closeCursor();
}
super.onDestroy();
}
#Override
public Loader<Cursor> onCreateLoader(int id, Bundle args) {
return new CursorLoader(this,buildDataUri(),null,null,null,null);
}
#Override
public void onLoadFinished(Loader<Cursor> loader, Cursor cursor) {
mAdapter.swapCursor(cursor);
}
#Override
public void onLoaderReset(Loader<Cursor> loader) {
mAdapter.swapCursor(null);
}
CustomAdapter.java
public class CustomAdapter extends RecyclerView.Adapter<CustomAdapter.ViewHolder> {
Cursor cursor;
public void swapCursor(Cursor c){
cursor = c;
if(c != null){
cursor.moveToFirst();
...
}
notifyDataSetChanged();
}
#Override
public ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
CardView c = (CardView) LayoutInflater.from(parent.getContext()).inflate(R.layout.card_cardView, parent, false);
return new ViewHolder(c);
}
#Override
public void onBindViewHolder(ViewHolder viewHolder, int position) {
...
}
#Override
public int getItemCount() {
return cursor != null ? cursor.getCount() : 0;
}
public void closeCursor() {
cursor.close();
}
public static class ViewHolder extends RecyclerView.ViewHolder{
...
}
#Zoedia I think its how it works, when new items comes in obviously your current items will scroll down.
You can try a work around by not displaying new items in Recycler View as it comes while giving a Floating Button from top and asking user to click on button or Swipe-to-Refresh to load new items. This will help you read the stuff you are currently reading and new items will be displayed (Recycler View will be inflated with new Items) as soon as user click on that button.
Its similar to Facebook Android App implementation (New Stories Button) :
Edit 1:
To add an item at a particular position, you have to add items from your loader at the 0th/initial position as you want the new feed to be at top.
After that call the mAdapter.notifyDataSetChanged(); to reflect that changes in Recycler view with new elements.
If you were using the ArrayList<>, then you just have to call .add(listitem,position) method to add a single item or .addAll(list,position) to add all the ArrayList.
and then call mAdapter.notifyDataSetChanged();.
I have a SQLite database in my app for which I made a ContentProvider class.
I also have a RecyclerView into which I load an ArrayList of objects into its adapter to populate the RecyclerView.
Currently, when the activity starts I get a Cursor via my ContentProvider, loop through the Cursor to create an ArrayList of objects that I then set as part of my RecyclerView.Adapter.
All that works, but what I really want is for my RecyclerView to dynamically update as new data is loaded into the SQLite database via the content provider.
I have seen posts listing this library CursorRecyclerAdapter but I do not want to use it because you do not get the nice RecyclerView animations on insert/delete.
I was trying to somehow use the LoaderManager.LoaderCallbacks<Cursor> call back methods to get a cursor, convert to arraylist, then swap that in my RecyclerView adapter but couldn't figure it out.
Could someone please show me some example code on how to set it up in my Activity so that the RecyclerView will refresh when new data is written into the local database via a local content provider?
Here is what my RecyclerView.Adapter looks like:
public class MyAdapter extends RecyclerView.Adapter<AdapterTodoList.Holder> {
private List<TodoItem> itemList;
private Context mContext;
//data
String message;
Long datetime;
//this class takes a context and a list of the items you want to populate into the recycler view
public AdapterTodoList(Context context, List<TodoItem> itemList) {
this.itemList = itemList;
this.mContext = context;
}
#Override
public Holder onCreateViewHolder(ViewGroup viewGroup, int viewType) {
//our xml showing how one row looks
View row = LayoutInflater.from(viewGroup.getContext()).inflate(R.layout.recycler_view_todo_item, viewGroup, false);
Holder holder = new Holder(row);
return holder;
}
#Override
public void onBindViewHolder(Holder holder, final int position) {
holder.recyclerLinearLayout.setOnClickListener(new View.OnClickListener() {
#Override
public void onClick(View view) {
Toast.makeText(mContext, "Recycle Click" + position, Toast.LENGTH_SHORT).show();
}
});
//get one item
TodoItem data = itemList.get(position);
Log.d("Test", "onBindViewHolder position " + position);
message = data.getMessage();
datetime = data.getDatetime();
//convert long to date
String dateString = new SimpleDateFormat("MM/dd/yyyy").format(new Date(datetime));
//set the holder
holder.messageTextView.setText(message);
}
#Override
public int getItemCount() {
return itemList.size();
}
public class Holder extends RecyclerView.ViewHolder {
protected ImageView checkBoxImageView;
protected TextView messageTextView;
protected LinearLayout recyclerLinearLayout;
public Holder(View view) {
super(view);
//checkBoxImageView = (ImageView) view.findViewById(R.id.checkBoxImageView);
messageTextView = (TextView) view.findViewById(R.id.messageTextView);
//the whole view
recyclerLinearLayout = (LinearLayout) view.findViewById(R.id.recyclerItemLinearLayout);
}
}
}
Here is what my Activity looks like so far:
public class HomeRec extends AppCompatActivity implements LoaderManager.LoaderCallbacks<Cursor>{
private Toolbar mToolbar;
//recyclerview and adapter
private RecyclerView mRecyclerView;
private MyAdapter adapter;
//the swipe refresh layout that wraps the recyclerview
private SwipeRefreshLayout mSwipeRefreshLayout;
//this will hold all of our results from our query.
List<TodoItem> itemList = new ArrayList<TodoItem>();
private Cursor mCursor;
//resources from layout
EditText toDoEditText;
Button cancelButton;
Button addButton;
//variables
private String message;
private long datetime;
//loader
private SimpleCursorAdapter mTodoAdapter;
private static final int TODO_LOADER = 0;
// These indices are tied to Projection. If Projection changes, these
// must change.
public static final int COL_ID = 0;
public static final int COL_MESSAGE = 1;
public static final int COL_DATETIME = 2;
public static final int COL_CHECKED = 3;
#Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_home_rec);
mToolbar = (Toolbar) findViewById(R.id.app_bar);
//set the Toolbar as ActionBar
setSupportActionBar(mToolbar);
// Initialize recycler view //
mRecyclerView = (RecyclerView) findViewById(R.id.todoRecyclerView);
mRecyclerView.hasFixedSize();
mRecyclerView.setLayoutManager(new LinearLayoutManager(this));
//set a grey line divider for each item in recycler view
mRecyclerView.addItemDecoration(
new DividerItemDecoration(this, null, false, true));
// END Initialize recycler view //
//initiate the swipe to refresh layout
mSwipeRefreshLayout = (SwipeRefreshLayout) findViewById(R.id.swipeRefreshLayout);
mSwipeRefreshLayout.setOnRefreshListener(new SwipeRefreshLayout.OnRefreshListener() {
#Override
public void onRefresh() {
// Refresh items
refreshItems();
}
void refreshItems() {
// Load items
// ...
// Load complete
onItemsLoadComplete();
}
void onItemsLoadComplete() {
// Update the adapter and notify data set changed
// ...
// Stop refresh animation
mSwipeRefreshLayout.setRefreshing(false);
}
});
//set colors for swipe to refresh
mSwipeRefreshLayout.setColorSchemeResources(
R.color.refresh_progress_2,
R.color.refresh_progress_3);
//fire my asynctask to get data for the first time
new MessagesAsyncTask().execute();
}
#Override
public boolean onCreateOptionsMenu(Menu menu) {
// Inflate the menu; this adds items to the action bar if it is present.
getMenuInflater().inflate(R.menu.menu_home_rec, menu);
return true;
}
#Override
public boolean onOptionsItemSelected(MenuItem item) {
// Handle action bar item clicks here. The action bar will
// automatically handle clicks on the Home/Up button, so long
// as you specify a parent activity in AndroidManifest.xml.
int id = item.getItemId();
//noinspection SimplifiableIfStatement
if (id == R.id.action_settings) {
return true;
}
return super.onOptionsItemSelected(item);
}
#Override
public Loader<Cursor> onCreateLoader(int id, Bundle args) {
//Not sure what to do here or how to make this work.
}
#Override
public void onLoadFinished(Loader<Cursor> loader, Cursor data) {
//Not sure what to do here or how to make this work.
}
#Override
public void onLoaderReset(Loader<Cursor> loader) {
//Not sure what to do here or how to make this work.
}
public class MessagesAsyncTask extends AsyncTask<Void, Void, List<TodoItem>> {
//the cursor for the query to content provider
private Cursor mCursor;
#Override
protected void onPreExecute() {
}
#Override
protected List<TodoItem> doInBackground(Void... params) {
// A "projection" defines the columns that will be returned for each row
String[] projection =
{
DataProvider.COL_ID, // Contract class constant for the COL_ID column name
DataProvider.COL_MESSAGE, // Contract class constant for the COL_MESSAGE column name
DataProvider.COL_DATETIME, // Contract class constant for the COL_DATETIME column name
DataProvider.COL_CHECKED // Contract class constant for the COL_CHECKED column name
};
// Defines a string to contain the selection clause
String selectionClause = null;
// An array to contain selection arguments
String[] selectionArgs = null;
// An ORDER BY clause, or null to get results in the default sort order
String sortOrder = DataProvider.COL_DATETIME + " DESC";
// Does a query against the table and returns a Cursor object
mCursor = getContentResolver().query(
DataProvider.CONTENT_URI_TODO, // The content URI of the Todo table
projection, // The columns to return for each row
selectionClause, // Either null, or the word the user entered
selectionArgs, // Either empty, or the string the user entered
sortOrder); // The sort order for the returned rows
// Some providers return null if an error occurs, others throw an exception
if (null == mCursor) {
// Insert code here to handle the error.
} else if (mCursor.getCount() < 1) {
// If the Cursor is empty, the provider found no matches
} else {
// Insert code here to do something with the results
}
//convert cursor to arraylist of objects
while (mCursor.moveToNext()) {
itemList.add(new TodoItem(mCursor.getInt(mCursor.getColumnIndex(DataProvider.COL_ID)),
mCursor.getString(mCursor.getColumnIndex(DataProvider.COL_MESSAGE)),
mCursor.getLong(mCursor.getColumnIndex(DataProvider.COL_DATETIME)),
mCursor.getInt(mCursor.getColumnIndex(DataProvider.COL_CHECKED))
));
}
mCursor.close();
return itemList;
}
#Override
protected void onPostExecute(List<TodoItem> itemList) {
if (!itemList.isEmpty()) {
adapter = new MyAdapter(HomeRec.this, itemList);
mRecyclerView.setAdapter(adapter);
} else {
Toast.makeText(getApplicationContext(), "No data to display", Toast.LENGTH_LONG).show();
}
}
}
}
I m not sure what you need but I think you should add this method To adapter and call once your data was pulled
public void swapItems(List< TodoItem > todolist){
this.mTodoList = todolist;
notifyDataSetChanged();
}
Hope this would help :D
from your question I assume that you are loading the data from the database and somewhere there is a code that is updating the database. And on every update you want to update your RecyclerView, If this is the case continue reading. I am not going to explain this completely but there are a lot of source that will explain you this.
Use BroadcastReciever : In the place where you are updating your database sendBroadcast(). And in the activity use the BroadcastReceiver
example and in the onReceive() function call load the data in your ArrayList and call the adapter.notifyDataSetChanged()
Instead of making new adapter each time in onPostExecute and set it to recyclerview again you can notify adapter after modifying list elements.
OR
If you want to make adapter using arraylist instead of cursoradapter using loader i have made sample for you with data provided by you. You can use this as a reference:
public class DataBaseActivity extends AppCompatActivity implements LoaderManager.LoaderCallbacks<Cursor> {
private List itemList;
private MyAdapter mAdapter;
#Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_data_base);
RecyclerView recycle=(RecyclerView)findViewById(R.id.rv_data);
SwipeRefreshLayout swipeRefreshLayout= (SwipeRefreshLayout) findViewById(R.id.srl_data);
recycle.setLayoutManager(new LinearLayoutManager(this));
itemList=new ArrayList();
mAdapter= new MyAdapter(this, itemList);
recycle.setAdapter(mAdapter);
swipeRefreshLayout.setOnRefreshListener(new SwipeRefreshLayout.OnRefreshListener() {
#Override
public void onRefresh() {
getContentResolver().notifyChange(DataProvider.CONTENT_URI_TODO, null); //if you are using content provider
//getSupportLoaderManager().restartLoader(100, null, DataBaseActivity.this); // if you are using support lib
//getLoaderManager().restartLoader(100, null, DataBaseActivity.this); //if you are not using support lib
}
});
// getLoaderManager().initLoader(100, null, this); //if you are not using support lib
getSupportLoaderManager().initLoader(100, null, this);
}
#Override
public Loader<Cursor> onCreateLoader(int id, Bundle args) {
String[] projection =
{
DataProvider.COL_ID, // Contract class constant for the COL_ID column name
DataProvider.COL_MESSAGE, // Contract class constant for the COL_MESSAGE column name
DataProvider.COL_DATETIME, // Contract class constant for the COL_DATETIME column name
DataProvider.COL_CHECKED // Contract class constant for the COL_CHECKED column name
};
// Defines a string to contain the selection clause
String selectionClause = null;
// An array to contain selection arguments
String[] selectionArgs = null;
// An ORDER BY clause, or null to get results in the default sort order
String sortOrder = DataProvider.COL_DATETIME + " DESC";
return new CursorLoader(this,DataProvider.CONTENT_URI_TODO, // The content URI of the Todo table
projection, // The columns to return for each row
selectionClause, // Either null, or the word the user entered
selectionArgs, // Either empty, or the string the user entered
sortOrder);
}
#Override
public void onLoadFinished(Loader<Cursor> loader, Cursor data) {
if(data!=null && data.getCount()>0)
{
itemList.clear();
while (data.moveToNext()) {
itemList.add(new TodoItem(data.getInt(data.getColumnIndex(DataProvider.COL_ID)),
data.getString(data.getColumnIndex(DataProvider.COL_MESSAGE)),
data.getLong(data.getColumnIndex(DataProvider.COL_DATETIME)),
data.getInt(data.getColumnIndex(DataProvider.COL_CHECKED))
));
}
}
else
Toast.makeText(getApplicationContext(), "No data to display", Toast.LENGTH_LONG).show();
if(data!=null)
data.close();
mAdapter.notifyDataSetChanged();
}
#Override
public void onLoaderReset(Loader<Cursor> loader) {
}
}
For "listening" to your ContentProvider changes, you'll could try to integrate ContentObserver into your ContentProvider, so it can trigger the necessary events when a transaction is done on your ContentProvider. After which, you'll declare an ContentObserver to your CONTENT_URI, then you can trigger an update to your RecyclerView.
More info on implementing ContentObserver here.
A sample code for updating an item in your RecyclerView would be,
public void update(T data){
synchronized (mLock){
if(data != null && mData.contains(data)){
int index = mData.indexOf(data);
mData.set(index, data);
notifyItemChanged(index);
}
}
}
Wherein T is the type of object if your row returns, mLock is just an instance object to acquire a lock, mData the list of items you've provided to your RecyclerView. You get the gist. :D
Hope it helps.
Refresh cursor every second
final Handler handler = new Handler();
final int delay = 1000; //milliseconds
handler.postDelayed(new Runnable(){
public void run(){
//Call cursor loader to refresh cursor
getSupportLoaderManager().restartLoader(LOADER_ID, null, MainActivity.this);
handler.postDelayed(this, delay);
}
}, delay);