Android - RecyclerView - Scrolling to specific textview - android

In my application, I want to scroll to a specific TextView in a RecyclerView if an intent specifies to scroll there. The original implementation involved looking at a listView, and looking at each item in it until I found a string matching the expected TextView string. I'm not sure how to replicate this for a RecyclerView. The closest I got to it was to look at the PreferenceGroupAdapter, but it seems to be a restricted class.

Create a method that returns the position of that TextView within your adapter. For this example, I created a very simple RecyclerView.Adapter that just holds a List<String> items and is looking for the string "target".
private int getTargetPosition(RecyclerView recycler) {
MyAdapter adapter = (MyAdapter) recycler.getAdapter();
for (int i = 0; i < adapter.items.size(); i++) {
if (adapter.items.get(i).equals("target")) {
return i;
}
}
throw new AssertionError("target is guaranteed to be in the list");
}
After that, it's as easy as calling scrollToPosition():
int position = getTargetPosition(recycler);
recycler.scrollToPosition(position);

Related

Save new positions in Room after recyclerView drag & drop

I've implemented a recyclerView with a drag and drop feature in my app. Everything works fine until the app is relaunched --any drag and drop changes were not saved/remembered by the app.
I've tried:
Using SharedPreference + GSON
Reading other SQLite answers here on SO like this one: Store new position of RecyclerView items in SQLite after being dragged and dropped
Reading Paul Burke's Medium Post
My current code looks like this:
In onCreate
ItemViewModel itemViewModel = ViewModelProviders.of(this).get(ItemViewModel.class);
itemViewModel.getAllItems().observe(this, new Observer<List<Item>>() {
#Override
public void onChanged(List<Item> items) {
adapter.setItemList(items);
itemList = items; //Global variable itemList
}
});
The method called when item is dragged/moved
private void swap(int firstPosition, int secondPosition) {
Collections.swap(itemList, firstPosition, secondPosition);
for(int i = 0; i < itemList.size(); i++) {
Item currentItem = itemList.get(i);
ItemViewModel.update(currentItem );
}
adapter.notifyItemMoved(firstPosition, secondPosition);
}
Any ideas how I can let my app save the reordered recyclerView after drag and drop? Thanks in advance.
If you wish to preserve the order of your items once you close your app or move to another activity, you will need to add a property to your entity files which will server as an order value for it. Once added, you could access your dao and run an UPDATE SQL in the Collections.swap method when reordering items and than on every load of the list, return the items ordered by that property.

Listview data model updated but items not displaying

I have searched these forums for nearly 3 hours and seen several similar questions but none of the answers works for me.
I have a single Activity, with several card views. One of the card views has a Spinner with string values and a very simple ListView. The user selects a value from the Spinner, between 1 and 12. The ListView should then display a number of strings equal to the value selected, based on the position in the spinner list. For example, user selects 2nd item in spinner list and the ListView displays 2 strings. I have a custom adapter on the listview. The ListView itself initially displays a single row, which is correct. However, after the user selects a value from the spinner, the listview is not displaying the extra rows, it still only displays one row. The data for the ListView comes from an ArrayList. I have checked the data model of the adapter after the user selects a value and it has the correct number of entries, as does the ArrayList itself, yet no matter what I try the ListView itself still only display the first row. I have tried NotifyDataSetChanged and every variation of Invalidate without success.
The various code samples:
#Override
public void onItemSelected(AdapterView<?> parent, View view, int position, long id) {
if (parent == spDoseFrequency){
Toast.makeText(this,String.valueOf(position),Toast.LENGTH_SHORT).show();
rebuildReminderTimesList(position + 1);
}
}
private void rebuildReminderTimesList(int numberOfTimes){
Toast.makeText(this,"yup",Toast.LENGTH_SHORT).show();
//reset selected item to position 1
myApp.iSelectedReminderTimeIndex = 0;
//clear array and list, then rebuild with hourly timeslots
iarrTimes = new int[numberOfTimes][2];
liReminderTimes.clear();
int startTime = 8;
for (int i = 0; i < numberOfTimes; i++){
iarrTimes[i][0] = startTime + i;
iarrTimes[i][1] = 0;
liReminderTimes.add(pad(startTime + i) + ":00");
}
//refresh the listview
myAdapter.notifyDataSetChanged();
}
public class ReminderListAdapter extends ArrayAdapter {
List<String> liTimes;
Context ctx;
LayoutInflater inf;
public ReminderListAdapter(Context ctx, List<String> liTimes) {
super(ctx, R.layout.reminder_time_listview, liTimes);
this.liTimes = liTimes;
this.ctx = ctx;
inf = LayoutInflater.from(ctx);
}
public void setLiTimes(List<String> liTimes){
this.liTimes = liTimes;
}
#Override
public View getView(int position, View convertView, ViewGroup parent) {
View view = convertView;
ViewHolder viewHolder;
if (view == null){
view = inf.inflate(R.layout.reminder_time_listview,parent,false);
viewHolder = new ViewHolder();
viewHolder.sTime = (TextView) view.findViewById(R.id.tvTime);
view.setTag(viewHolder);
} else {
viewHolder = (ViewHolder) view.getTag();
}
viewHolder.sTime.setText(liTimes.get(position));
return view;
}
private static class ViewHolder{
TextView sTime;
}
}
Any help would be appreciated as this is driving me crazy.
Quick update to this question: I have just tested supplying the initial list more than one value but even then it only displays the first item. Is there perhaps a problem with using ListView inside a CardView object? All my other cards work fine, only the ListView one fails to display properly.
Also, I have tried amending the code so that instead of changing the number of elements in the list, it just changes the text in the string of the first element and this works fine. So the notifyDataSetChanged appears to be working, but it just won't display more than one item. A quick check of the Adapter.getCount() method also gives the correct number of elements back, but won't display them.
A lot of folks forget to do the notifyDataSetChanged() call, but you've got that. Are you using a custom adapter? If so, that makes this sound like an issue with one or more of the adapter's methods. In particular, it sounds like getCount or getView might not be returning what they should be. That could either be because of a flawed logic issue, the underlying data source isn't being updated correctly, or the underlying data source isn't the same object you think it is. Without seeing your adapter though, it's hard to diagnose.
I found the problem. I had several CardView objects inside a LinearLayout, which itself was inside a ScrollView. As soon as I removed the ScrollView, the List inside the Card displayed properly. This has unfortunately created another problem in that I can no longer scroll the page to see later cards, which I have not yet found a solution for, but that is a different topic.

RecyclerView change data set

I want to implement search functionality for my RecyclerView. On text changed i want to change the data that are displayed with this widget. Maybe this question has been asked before or is simple, but I don't know how the change the data that is to be shown...
My RecyclerView is defined as follows:
// 1. get a reference to recyclerView
mRecyclerView = (RecyclerView)findViewById(R.id.recyclerView);
// 2. set layoutManger
mRecyclerView.setLayoutManager(new LinearLayoutManager(this));
// 3. create an adapter
mAdapter = new ItemsAdapter(itemsData);
// 4. set adapter
mRecyclerView.setAdapter(mAdapter);
And the data that I am showing is something like:
ItemData itemsData[] = { new ItemData("Mary Richards"),
new ItemData("Tom Brown"),
new ItemData("Lucy London")
};
So when when I want to give the adapter another set of data, another array (with one item for example), what should I do?
If you have stable ids in your adapter, you can get pretty good results (animations) if you create a new array containing the filtered items and call
recyclerView.swapAdapter(newAdapter, false);
Using swapAdapter hints RecyclerView that it can re-use view holders. (vs in setAdapter, it has to recycle all views and re-create because it does not know that the new adapter has the same ViewHolder set with the old adapter).
A better approach would be finding which items are removed and calling notifyItemRemoved(index). Don't forget to actually remove the item. This will let RecyclerView run predictive animations. Assuming you have an Adapter that internally uses an ArrayList, implementation would look like this:
// adapter code
final List<ItemData> mItems = new ArrayList(); //contains your items
public void filterOut(String filter) {
final int size = mItems.size();
for(int i = size - 1; i>= 0; i--) {
if (mItems.get(i).test(filter) == false) {
mItems.remove(i);
notifyItemRemoved(i);
}
}
}
It would perform even better if you can batch notifyItemRemoved calls and use notifyItemRangeRemoved instead. It would look sth like: (not tested)
public void filterOut(String filter) {
final int size = mItems.size();
int batchCount = 0; // continuous # of items that are being removed
for(int i = size - 1; i>= 0; i--) {
if (mItems.get(i).test(filter) == false) {
mItems.remove(i);
batchCount ++;
} else if (batchCount != 0) { // dispatch batch
notifyItemRangeRemoved(i + 1, batchCount);
batchCount = 0;
}
}
// notify for remaining
if (batchCount != 0) { // dispatch remaining
notifyItemRangeRemoved(0, batchCount);
}
}
You need to extend this code to add items that were previously filtered out but now should be visible (e.g. user deletes the filter query) but I think this one should give the basic idea.
Keep in mind that, each notify item call affects the ones after it (which is why I'm traversing the list from end to avoid it). Traversing from end also helps ArrayList's remove method performance (less items to shift).
For example, if you were traversing the list from the beginning and remove the first two items.
You should either call
notifyItemRangeRemoved(0, 2); // 2 items starting from index 0
or if you dispatch them one by one
notifyItemRemoved(0);
notifyItemRemoved(0);//because after the previous one is removed, this item is at position 0
This is my answer - thanks to Ivan Skoric from his site: http://blog.lovelyhq.com/creating-lists-with-recyclerview-in-android/
I created an extra method inside my adapter class:
public void updateList(List<Data> data) {
mData = data;
notifyDataSetChanged();
}
Then each time your data changes, you just call this method passing in your new data and your view should change to reflect it.
Just re-initialize your adapter:
mAdapter = new ItemsAdapter(newItemsData);
or if you only need to remove add a few specific items rather than a whole list:
mAdapter.notifyItemInserted(position);
or
mAdapter.notifyItemRemoved(position);
If you want to change the complete Adapter in the recycler view. you can just simply set by recycler.setAdapter(myAdapter);
It will automatically remove the old adapter from recycler view and replace it with your new adapter.
As ygit answered, swapAdapter is interesting when you have to change the whole content.
But, in my FlexibleAdapter, you can update the items with updateDataSet. You can even configure the adapter to call notifyDataSetChanged or having synchronization animations (enabled by default). That, because notifyDataSetChanged kills all the animations, but it's good to have for big lists.
Please have a look at the description, demoApp and Wiki pages: https://github.com/davideas/FlexibleAdapter

Move item to top of listview when a new message arrives

I am working on a XMPP based chat in android.. and I am struck at a point where I need to update the position of an item in the listview to the top in case a new.message arrives.
The use case is.. I am on Contacts screen of the app and a new message comes.. so this contact should move to top of the list and get bold. This is what is similar to whatsapp as well
How can this be done. My class imolemebts activity and i have implemented custom list adapter.
So howcan I find if an item exists in the listview and secondly how to dynamically change position
First, keep in mind that a ListView is just a representation of a list of Objects. So if you want to know if an item is in the ListView, you just have to check if the corresponding Object is in your list of Objects.
Is the same when you want to change the position of one item, you have to change the position of the Object in the list.
Start by defining these objects:
private ArrayList<MyObject> lists = new ArrayList<MyObject>();
private MyCustomAdapter myAdapter;
The first time you create your ListView, just do as usually:
//fill your list with your objects
lists.add(myObject1);
lists.add(myObject2);
lists.add(myObject3);
//create and set the adapter
myAdapter = new MyCustomAdapter(..., ..., lists);
myListView.setAdapter(myAdapter);
Now you can know if your lists contains a specific object (which is the same that checking if an item is in your ListView) by simply testing that:
lists.contains(anObject);
Then, if you want to change the position of a specific item in the ListView, you have to create a new list and put the elements in the correct order. You can use something like that (not tested but it should work):
private ArrayList<MyObject> moveItemToTop(ArrayList<MyObject> lists, int positionOfItem) {
if (lists == null || positionOfItem < 0 || positionOfItem >= lists.size()) {
return lists;
}
ArrayList<MyObject> sortedList = new ArrayList<MyObject>();
//add the item to the top
sortedList.add(lists.get(positionOfItem));
for (int i=0; i<lists.size(); i++) {
if (i != positionOfItem) {
sortedList.add(lists.get(i));
}
}
return sortedList;
}
Or even this (which is way easier...).
Finally, call these two methods to update your ListView:
myAdapter = new MyCustomAdapter(..., ..., moveItemToTop(lists, itemPosition));
myListView.setAdapter(myAdapter);
This is how I resolved it
private void moveMessageToTop(MessageObject message) {
int index = 0;
for (Friends friend : mFriends) {
if (friend.getName().equalsIgnoreCase(message.getFrom().split("#")[0])) {
index = mFriends.indexOf(friend);
break;
}
}
if (index != 0) {
mFriends.add(0,new Friends(message.getFrom().split("#")[0], message
.getMessage()));
} else {
Friends frnd = mFriends.get(index);
frnd.setStatus(message.getMessage());
mFriends.add(0, frnd);
mFriends.remove(index);
}
((ListAdapter) lvFriends.getAdapter()).notifyDataSetChanged();
}

Click all the list view elements while scrolling using robotium

I have a listView that contains lots of elements i.e. we have to scroll down to see all the elements. Now what i want to do is, click all the listView elements. How can I do that. Right now,I am using the following code but it doesn't scroll automatically. Please help.
ListView l = solo.getCurrentListViews().get(0);
assertNotNull("No list views!", l);
assertTrue("No items in list view!", l.getChildCount() > 0);
// Get the last list item
View v = l.getChildAt(l.getChildCount());
System.out.println("getChildCount: " + l.getChildCount());
int i = 1;
while (i <= l.getChildCount()) {
solo.clickInList(i);
solo.goBack();
i++;
}
I have previously used these helper functions in a slightly different state to handle most of what we need with listviews:
public View getViewAtIndex(final ListView listElement, final int indexInList, Instrumentation instrumentation) {
ListView parent = listElement;
if (parent != null) {
if (indexInList <= parent.getAdapter().getCount()) {
scrollListTo(parent, indexInList, instrumentation);
int indexToUse = indexInList - parent.getFirstVisiblePosition();
return parent.getChildAt(indexToUse);
}
}
return null;
}
public <T extends AbsListView> void scrollListTo(final T listView,
final int index, Instrumentation instrumentation) {
instrumentation.runOnMainSync(new Runnable() {
#Override
public void run() {
listView.setSelection(index);
}
});
instrumentation.waitForIdleSync();
}
With these your method would be:
ListView list = solo.getCurrentListViews().get(0);
for(int i=0; i < list.getAdapter().getCount(); i++){
solo.clickOnView(getViewAtIndex(list, i, getInstrumentation()))
}
It looks like your code, as currently implemented, is only considering the visibile list items when controlling the loop and handling the clicking. It's important to note the behavior of two things:
First, there's a concept called view recycling in Android that helps conserve memory when dealing with ListViews. Only the views that are currently on screen are created, and once they scroll off the screen they'll be repopulated with new data. Therefore, calling methods like getChildCount and getChildAt on a ListView will only perform these operations on the visible items. To find information about the data that populates the list, you can call methods such as getCount() or getItem() on the ListView's adapter.
Second, the clickInList() method is 1-indexed, relative to the current position of the list, and can only be used for visible items. As far as I know, it will never scroll your list automatically. This means that calling clickInList(2) when at the top of the list will click the second item, but then calling clickInList(2) again when the 30th item is at the top of the list will click the 32nd.
Knowing these two things, your solution will need to consider all of the list data and perhaps have a bit more precision when making clicks. Here's how I'd rewrite your while loop to ensure you'll be able to handle every item on the list, hope this helps:
ListAdapter adapter = l.getAdapter();
for(int i=0; i < adapter.getCount(); i++)
{
//Scroll down the list to make sure the current item is visible
solo.scrollListToLine(l, i);
//Here you need to figure out which view to click on.
//Perhaps using adapter.getItem() to get the data for the current list item, so you know the text it is displaying.
//Here you need to click the item!
//Even though you're in a list view, you can use methods such as clickOnText(), which might be easier based on how your adapter is set up
solo.goBack();
}
It should help you(not tested):
public void clickAllElementsOnListView(int index) {
ListView listView = solo.getCurrentListViews().get(index);
count = listView.getAdapter() != null ? listView.getAdapter().getCount() : 0;
for (int i = 0; i < count; i++) {
scrollListToLine(listView, i);
solo.clickInList(1, index);
solo.goBack();
}
}
protected void scrollListToLine(final ListView listView, final int line) {
getInstrumentation().runOnMainSync(new Runnable() {
public void run() {
listView.setSelection(line);
}
});
}

Categories

Resources