I am using this tutorial to build an itemTouchListener for my RecyclerView. The recyclerview is filled with more items than the screen fits (more than 10), so recycling gets into action. The itemtouchHelper handles both up-down and left-right movement. After 2 days of struggle (had setStableIds to true which caused flickering of the viewholder views when the were moved up-down), I finally got a better behaviour. My code of the crucial features:
#Override
public int getItemCount() {
return questionlist.size();
}
#Override
public void onViewMoved(int oldPosition, int newPosition) {
targetqueobj = questionlist.get(oldPosition);
this.fromPosition = oldPosition;
this.toPosition = newPosition;
questionlist.remove(targetqueobj);
questionlist.add(newPosition, targetqueobj);
// targetqueobj.setinturn(toPosition+1);
}
#Override
public void onViewSwiped(final RecyclerView.ViewHolder thisviewholder,final int position, int direction) {
targetqueobj = questionlist.get(position);
if (direction == ItemTouchHelper.LEFT){
// saveqset();
Intent intent = new Intent(context, QuestionEditActivity.class);
intent.putExtra("com.logictop.mqapp.QuestionObjParcelable",targetqueobj);
context.startActivity(intent);
}
if (direction == ItemTouchHelper.RIGHT) {
// DIALOG
AlertDialog.Builder builder = new AlertDialog.Builder(context);
builder.setMessage(R.string.remove_question);
builder.setNegativeButton(R.string.No, new DialogInterface.OnClickListener() {
public void onClick(DialogInterface arg0, int arg1) {
// thisviewholder.itemView.setAlpha(1);
notifyDataSetChanged();
}
});
builder.setPositiveButton(R.string.Yes, new DialogInterface.OnClickListener() {
public void onClick(DialogInterface arg0, int arg1) {
questionlist.remove(position);
notifyItemRemoved(position);
notifyItemRangeChanged(position, getItemCount()-position);
}
});
Dialog dialog = builder.create();
dialog.setCancelable(false);
dialog.setCanceledOnTouchOutside(false);
dialog.show();
}
}
if (direction == ItemTouchHelper.RIGHT) {
// DIALOG
AlertDialog.Builder builder = new AlertDialog.Builder(context);
builder.setMessage(R.string.remove_question);
builder.setNegativeButton(R.string.No, new DialogInterface.OnClickListener() {
public void onClick(DialogInterface arg0, int arg1) {
thisviewholder.itemView.setAlpha(1);
notifyDataSetChanged();
}
});
builder.setPositiveButton(R.string.Yes, new DialogInterface.OnClickListener() {
public void onClick(DialogInterface arg0, int arg1) {
questionlist.remove(position);
notifyItemRemoved(position);
notifyItemRangeChanged(position, getItemCount()-position);
}
});
Dialog dialog = builder.create();
dialog.setCancelable(false);
dialog.setCanceledOnTouchOutside(false);
dialog.show();
}
}
The problem is this. Though the recyclerview runs smoothly with the items very nicely changing positions, when an item is swiped away, sometimes another item looks also removed when the recyclerview is scrolled down (so it seems that the other item that gets removed fills the same "screen" position in the recyclerview after it is scrolled). It doesn't happen every time and it mostly happens when a view in some specific positions are swiped away. That "gap" can be mended if I move a neighbor view up or down.
I have tried every solution I found in here (notifyDataChanged, notifyItemRangeChanged (with every parameter compination). But nothing could get me a stable behaviour. AFter a lot of stackoverflow searching I decided to follow the advice of holder.setIsRecyclable(false)
even though I didn't want to do that (as it eliminates the point of having a recyclerview after all). But in that case, other problems appear. You'll see below that when most of the views are swiped away, they lower ones won't leave the screen, even though they apparently have "left the adapter" (cannot swipe them away). And in the end, the last views stay stuck.
I have tested both ways on a completely new project with nothing external coming into the way. I have tried putting notifyDataChanged() in an overriden function of clearView. Nothing seems to provide a rock solid stable recyclerview that won't get at some point a gap in itself.
The question now: is there a way to make a recyclerview with working recycling behave like it is supposed to behave or should I accept that situation?
Thank you very much for your attention!
UPDATE 1----------------------------
As I was told there could be an issue with this thisviewholder.itemView.setAlpha(1); I commented it out along with the corresponding overriden onChildDraw (the whole of it) that is used to fade the view out when it is swiped out. So now nothing gets invisible. Still the problem persists.
UPDATE 2----------------------------
I have enforced stableIds (had already done that once and it didn't work)
#Override
public long getItemId(int position){
return questionlist.get(position).getquestionid();
}
and the adapter contructor
public QSetEditAdapter(ArrayList<QuestionObj_prcl> questionlist, Context context) {
this.context = context;
this.questionlist = questionlist;
setHasStableIds(true);
}
Still the problem persists.
UPDATE 3----------------------------
I ended up using a different tutorial and now everything works as expected. Thanks a lot!
Change your adapter this way:
Set hasStableIds() to true.
Implement getItemId(int) and return unique ids for your items.
I don't know why this is happening
Exactly in the method below the list, it keeps the value you gave it for the first time and does not update this value !!
public void onViewSwiped (int position)
But what is the solution???
The solution is to use the -> touchHelper.startSwipe(holder) method
For example :
holder.itemView.setOnTouchListener (new View.OnTouchListener () {
VerOverride
public boolean onTouch (View view, MotionEvent event) {
if (event.getAction () == MotionEvent.ACTION_DOWN) {
Update = false;
touchHelper.startSwipe (holder);
}
return true;
}
});
The job of this method is to reload the deleted animations and views
Good luck
Related
I want to implement this
.
I want to check if the clicked item is fully visible and if it's not I would like to smoothly scroll upwards/downwards. I have a GridLayoutManager with 3 columns. The items are all of the same size, and are just ImageViews.
I am able to get the RecyclerView to scroll with:
#Override
public void onClick(View view) {
int adapterPosition = RecHolder.this.getAdapterPosition();
mRecyclerView.scrollToPosition(adapterPosition);
...
}
But it's not a "scroll", it's very laggish and way too quick.
If I try to use mRecyclerView.smoothScrollToPosition(adapterPosition); the result is the same. Exactly the same movement, there is no visible difference.
Don't bother testing to see if it's not completely visible. Just insert a command to scroll to that position. Since you didn't post any of your code I can't specifically say how you would do it. I personally have my adapter create an intent and my activity handles the intent. If that's what you're doing then you can include the getAdapterPosition() as an extra like this (vh is my ViewHolder).
vh.mImageView.setOnClickListener(new View.OnClickListener() {
#Override
public void onClick(View v) {
SelectItemGridAdapter.ContentViewHolder vh = (SelectItemGridAdapter.ContentViewHolder) (((View)v.getParent()).getTag());
Intent intent = new Intent(ACTION_SELECT_Item);
intent.putExtra(Constants.MSG_TYPE, SELECT_ITEM_TAPPED);
intent.putExtra(SELECT_ITEM_TAPPED_ID, vh.viewModel.mItemId);
intent.putExtra(SELECT_ITEM_TAPPED_POS, vh.getAdapterPosition());
LocalBroadcastManager.getInstance(mContext).sendBroadcast(intent);
}
});
Then the reveiver in the activity can get the SELECT_ITEM_TAPPED_POS value...
int pos = intent.getIntExtra(SelectItemGridAdapter.SELECT_ITEM_TAPPED_POS, -1);
if (pos > -1)
rv.scrollToPosition(pos);
HTH, Mike
So after calling this onto my tests to layout the recyclerview:
recyclerView.addOnItemTouchListener(listener);
recyclerView.measure(0, 0);
recyclerView.layout(0, 0, 100, 10000);
and my OnItemTouchListener being:
listener = new RecyclerOnTouchListener(this, new RecyclerOnTouchListener.OnItemClickListener() {
#Override
public void onItemClick(View v, int position) {
Toast.makeText(this, "Sample toast", Toast.LENGTH_SHORT).show();
}
});
and I want to assert:
Assertions.assertThat(ShadowToast.getTextOfLatestToast()).isEqualToIgnoringCase("Sample Toast");
I need to simulate item clicks on the recyclerview. And what i'm doing is this:
recyclerView.findViewHolderForAdapterPosition(0).itemView.performClick();
recyclerView.performClick()
and they both return false, meaning no onclick listener in them is being called.
So how do we really test OnItemTouchListener in recyclerview? In Robolectric?
Any help would be appreciated. Thanks!
I had a similar issue that was fixed by adding the line before setAdapter:
recyclerView.setLayoutManager(LinearLayoutManager(this))
I've run in to the same problem and while I haven't found any good solution to the problem, I have a workaround that at least makes testing possible until I can find something better.
One thing you can do is to create a method in your activity and have the onClick listener call that directly.
void recyclerOnClick(View view, int position) {
Toast.makeText(this, "Click", Toast.LENGTH_SHORT).show();
}
void addTouchListener() {
RecyclerOnTouchListener touchListener = new RecyclerOnTouchListener(this, recyclerView,
new RecyclerOnTouchListener.OnItemClickListener() {
#Override
public void onClick(#NotNull View view, int position) {
recyclerOnClick(view, position);
}
});
recyclerView.addOnItemTouchListener(touchListener)
}
Then in your tests you can just call adapter.recyclerOnClick() for the view you want to simulate the click on.
I have implemented a ListView that has the functionality that you see in many apps, where user scrolls to bottom and it loads more, that OnScrollListener is this:
public class OnScrolledToEndListener implements AbsListView.OnScrollListener
{
private int prevLast;
#Override
public void onScrollStateChanged(AbsListView absListView, int i)
{
}
#Override
public void onScroll(AbsListView absListView, int first, int visible, int total)
{
int last = first + visible;
if (last == total)
{
if (prevLast != last)
{
prevLast = last;
onScrolledToEnd();
}
}
}
public void onScrolledToEnd()
{
}
}
Now the problem is that when a user has scrolled to the bottom of a list, and hits the refresh button in my app, I want it to start over at the top of the list, because if it stays at the bottom of the list, then the scroll listener will immediately trigger. The best way I've found to solve this is by doing the following before executing the refresh:
mListView.setSelection(0);
mListView.post(
new Runnable()
{
#Override
public void run()
{
mListView.setVisibility(View.GONE);
mLoadingLayout.setVisibility(View.VISIBLE); //this is basically a progressbar
// do the refresh
}
}
);
But there is a slight flicker when the list scrolls to the top. Any ideas on how to make it look better?
I figured out the solution. Apparently setting the ListView to View.GONE makes it not update its layout, so I set it to View.INVISIBLE instead and it worked. I didn't even have to use a Runnable.
mListView.setSelection(0);
mListView.setVisibility(View.INVISIBLE);
mLoadingLayout.setVisibility(View.VISIBLE);
I'm building an interface similar to the Google Hangouts chat interface. New messages are added to the bottom of the list. Scrolling up to the top of the list will trigger a load of previous message history. When the history comes in from the network, those messages are added to the top of the list and should not trigger any kind of scroll from the position the user had stopped when the load was triggered. In other words, a "loading indicator" is shown at the top of the list:
Which is then replaced in-situ with any loaded history.
I have all of this working... except one thing that I've had to resort to reflection to accomplish. There are plenty of questions and answers involving merely saving and restoring a scroll position when adding items to the adapter attached to a ListView. My problem is that when I do something like the following (simplified but should be self-explanatory):
public void addNewItems(List<Item> items) {
final int positionToSave = listView.getFirstVisiblePosition();
adapter.addAll(items);
listView.post(new Runnable() {
#Override
public void run() {
listView.setSelection(positionToSave);
}
});
}
Then what the user will see is a quick flash to the top of the ListView, then a quick flash back to the right location. The problem is fairly obvious and discovered by many people: setSelection() is unhappy until after notifyDataSetChanged() and a redraw of ListView. So we have to post() to the view to give it a chance to draw. But that looks terrible.
I've "fixed" it by using reflection. I hate it. At its core, what I want to accomplish is reset the first position of the ListView without going through the rigamarole of the draw cycle until after I've set the position. To do that, there's a helpful field of ListView: mFirstPosition. By gawd, that's exactly what I need to adjust! Unfortunately, it's package-private. Also unfortunately, there doesn't appear to be any way to set it programmatically or influence it in any way that doesn't involve an invalidate cycle... yielding the ugly behavior.
So, reflection with a fallback on failure:
try {
Field field = AdapterView.class.getDeclaredField("mFirstPosition");
field.setAccessible(true);
field.setInt(listView, positionToSave);
}
catch (Exception e) { // CATCH ALL THE EXCEPTIONS </meme>
e.printStackTrace();
listView.post(new Runnable() {
#Override
public void run() {
listView.setSelection(positionToSave);
}
});
}
}
Does it work? Yes. Is it hideous? Yes. Will it work in the future? Who knows? Is there a better way? That's my question.
How do I accomplish this without reflection?
An answer might be "write your own ListView that can handle this." I'll merely ask whether you've seen the code for ListView.
EDIT: Working solution with no reflection based on Luksprog's comment/answer.
Luksprog recommended an OnPreDrawListener(). Fascinating! I've messed with ViewTreeObservers before, but never one of these. After some messing around, the following type of thing appears to work quite perfectly.
public void addNewItems(List<Item> items) {
final int positionToSave = listView.getFirstVisiblePosition();
adapter.addAll(items);
listView.post(new Runnable() {
#Override
public void run() {
listView.setSelection(positionToSave);
}
});
listView.getViewTreeObserver().addOnPreDrawListener(new OnPreDrawListener() {
#Override
public boolean onPreDraw() {
if(listView.getFirstVisiblePosition() == positionToSave) {
listView.getViewTreeObserver().removeOnPreDrawListener(this);
return true;
}
else {
return false;
}
}
});
}
Very cool.
As I said in my comment, a OnPreDrawlistener could be another option to solve the problem. The idea of using the listener is to skip showing the ListView between the two states(after adding the data and after setting the selection to the right position). In the OnPreDrawListener(set with listViewReference.getViewTreeObserver().addOnPreDrawListener(listener);) you'll check the current visible position of the ListView and test it against the position which the ListView should show. If those don't match then make the listener's method return false to skip the frame and set the selection on the ListView to the right position. Setting the proper selection will trigger the draw listener again, this time the positions will match, in which case you'd unregister the OnPreDrawlistener and return true.
I was breaking up my head until I found a solution similar to this.
Before adding a set of items you have to save top distance of the firstVisible item and after adding the items do setSelectionFromTop().
Here is the code:
// save index and top position
int index = mList.getFirstVisiblePosition();
View v = mList.getChildAt(0);
int top = (v == null) ? 0 : v.getTop();
// for (Item item : items){
mListAdapter.add(item);
}
// restore index and top position
mList.setSelectionFromTop(index, top);
It works without any jump for me with a list of about 500 items :)
I took this code from this SO post: Retaining position in ListView after calling notifyDataSetChanged
The code suggested by the question author works, but it's dangerous.
For instance, this condition:
listView.getFirstVisiblePosition() == positionToSave
may always be true if no items were changed.
I had some problems with this aproach in a situation where any number of elements were added both above and below the current element. So I came up with a sligtly improved version:
/* This listener will block any listView redraws utils unlock() is called */
private class ListViewPredrawListener implements OnPreDrawListener {
private View view;
private boolean locked;
private ListViewPredrawListener(View view) {
this.view = view;
}
public void lock() {
if (!locked) {
locked = true;
view.getViewTreeObserver().addOnPreDrawListener(this);
}
}
public void unlock() {
if (locked) {
locked = false;
view.getViewTreeObserver().removeOnPreDrawListener(this);
}
}
#Override
public boolean onPreDraw() {
return false;
}
}
/* Method inside our BaseAdapter */
private updateList(List<Item> newItems) {
int pos = listView.getFirstVisiblePosition();
View cell = listView.getChildAt(pos);
String savedId = adapter.getItemId(pos); // item the user is currently looking at
savedPositionOffset = cell == null ? 0 : cell.getTop(); // current item top offset
// Now we block listView drawing until after setSelectionFromTop() is called
final ListViewPredrawListener predrawListener = new ListViewPredrawListener(listView);
predrawListener.lock();
// We have no idea what changed between items and newItems, the only assumption
// that we make is that item with savedId is still in the newItems list
items = newItems;
notifyDataSetChanged();
// or for ArrayAdapter:
//clear();
//addAll(newItems);
listView.post(new Runnable() {
#Override
public void run() {
// Now we can finally unlock listView drawing
// Note that this code will always be executed
predrawListener.unlock();
int newPosition = ...; // Calculate new position based on the savedId
listView.setSelectionFromTop(newPosition, savedPositionOffset);
}
});
}
I have a LinearLayout that contains some other views and among those a ListView.
This view is loaded from another one by clicking a button.
This button somehow specify what element in the ListView needs to be the first visible one in the list. The elements that populates the list are retrieved via HTTP from an external server.
The problem is that I can get the Nth element to be the first in the list.
Please note, I do not want to move it form it current position to a new one, I want the list to scroll.
I have tried with setSelected() and scrollTo(x,y) and scrollBy(x,y) but with no luck.
I have also gave a try to this pice of code, as ugly as it is, but I just wanted to try f it was working:
ListView categoryList = (ListView)findViewById(R.id.category_list);
categoryList.post(new Runnable() {
#Override
public void run() {
Log.d(this.getClass().getName(), "CategoryActivity.scrollToIndex: " + CategoryActivity.scrollToIndex);
if(CategoryActivity.scrollToIndex>0){
ListView categoryList = (ListView)findViewById(R.id.category_list);
categoryList.setScrollContainer(true);
categoryList.scrollTo(4, CategoryActivity.scrollToIndex * 50);
categoryList.requestLayout();
}
}
});
And this gave me some success, but the ListView was then behaving crazy in a way I am not even able to describe....
Any idea?
Try to add it to the message queue
categoryList.post(new Runnable() {
public void run() {
categoryList.scrollTo(4, CategoryActivity.scrollToIndex * 50);
}
});
It worked for me in a ScrollView (check this answer).
i made functions that could be useful for others for listview scrolling, they work for me in every android version, emulator and device, here itemheight is the fixed height of view in the listview.
int itemheight=60;
public void scrollToY(int position)
{
int item=(int)Math.floor(position/itemheight);
int scroll=(int) ((item*itemheight)-position);
this.setSelectionFromTop(item, scroll);// Important
}
public void scrollByY(int position)
{
position+=getListScrollY();
int item=(int)Math.floor(position/itemheight);
int scroll=(int) ((item*itemheight)-position);
this.setSelectionFromTop(item, scroll);// Important
}
public int getListScrollY()
{
try{
//int tempscroll=this.getFirstVisiblePosition()*itemheight;// Important
View v=this.getChildAt(0);
int tempscroll=(this.getFirstVisiblePosition()*itemheight)-v.getTop();// Important
return tempscroll;
}catch(Exception e){}
return 0;
}