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);
Related
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
I have an Activity containing a ViewPager that displays N fragments. Each fragment is showing the properties of an object from an ArrayList in my ViewPager's custom adapter (extends FragmentStatePagerAdapter).
The fragment has (among other things) a button that should remove the currently displayed fragment and scroll to the next one with setCurrentItem(position, true) so that if the user scrolls back, the previous item is gone. I do so by using something like this (simplified):
deleteButton.setOnClickListener(new OnClickListener() {
#Override
public void onClick(View v) {
MyActivity parentActivity = (MyActivity)getActivity();
// First, scroll to next item (smoothly)
parentActivity.pager.setCurrentItem(parentActivity.pager.getCurrentItem()+1, true);
// Database stuff...
doSomeDBOperations();
// Method in Activity that removes the current object (I believe this method is working fine and yes, it calls notifyDataSetChanged())
parent.removeObject(currentObject);
}
});
This has the desired behavior as the object represented by the fragment whose delete button was pressed gets removed and the viewpager goes to the next page.
My problem is that the ViewPager doesn't scroll smoothly but rather "jumps instantly" to the next fragment. If I comment the removeObject() call, the smooth scroll works (but the item isn't removed). I believe it's has something to do with the removeObject() being called before the setCurrentItem() has finished the smooth scrolling animation?
Any ideas on how to fix this and achieve item removal + smooth scroll? If my assumption is correct, how can I make sure I get the smooth scroll to finish before removing the object?
EDIT 1:
My assumption seems correct. If I put the parent.removeObject(currentObject) inside
// ...inside the previously shown public void onClick(View v)...
confirm.postDelayed(new Runnable() {
#Override
public void run() {
// Method in Activity that removes the current object (I believe this method is working fine and yes, it calls notifyDataSetChanged())
parent.removeObject(currentObject);
}
}, 1000);
so that the removeObject() call waits for a second, it works as expected: scroll to the next item, remove the previous. But this is a very ugly workaround so I'd still like a better approach.
EDIT 2:
I figured out a possible solution (see below).
I ended up overriding the
public void onPageScrollStateChanged(int state)
method:
Whenever the user presses the delete button in the fragment, the listener sets a bool in the current item (flagging it for deletion) and scrolls to the next one.
When the onPageScrollStateChanged detects that the scroll state changed to ViewPager.SCROLL_STATE_IDLE (which happens when the smooth scroll ends) it checks if the previous item was marked for deletion and, if so, removes it from the ArrayList and calls notifyDataSetChanged().
By doing so, I've managed to get the ViewPager to smoothly scroll to the next position and delete the previous item when the "delete" button is pressed.
EDIT: Code snippet.
#Override
public void onPageScrollStateChanged(int state)
{
switch(state)
{
case ViewPager.SCROLL_STATE_DRAGGING:
break;
case ViewPager.SCROLL_STATE_IDLE:
int previousPosition = currentPosition - 1;
if(previousPosition < 0){
previousPosition = 0;
}
MyItem previousItem = itemList.get(previousPosition);
if(previousItem.isDeleted())
{
deleteItem(previousItem);
// deleteItem() Does some DB operations, then calls itemList.remove(position) and notifyDataSetChanged()
}
break;
case ViewPager.SCROLL_STATE_SETTLING:
break;
}
}
Have you tried ViewPager.OnPageChangeListener?
I would call removeObject(n) method in OnPageChangeListener.onPageSelected(n+1) method.
I did something different that works smoothly. The idea is to to remove the current item with animation (setting its alpha to 0), then translating horizontally the left or right item (with animation) to the now invisible item position.
After the animation is complete, I do the actual data removal and notfyDataSetChanged() call.
This remove() method I put inside a subclass of ViewPager
public void remove(int position, OnViewRemovedListener onViewRemovedListener) {
final int childCount = getChildCount();
if (childCount > 0) {
View toRemove = getChildAt(position);
int to = toRemove.getLeft();
final PagerAdapter adapter = getAdapter();
toRemove.animate()
.alpha(0)
.setDuration(getResources().getInteger(android.R.integer.config_shortAnimTime))
.setListener(new SimpleAnimatorListener() {
#Override
public void onAnimationEnd(Animator animation) {
if (childCount == 1) {
if (onViewRemovedListener != null) onViewRemovedListener.onRemoved(position, -1);
if (adapter!= null) adapter.notifyDataSetChanged();
}
}
})
.start();
if (childCount > 1) {
int newPosition = position + 1 <= childCount - 1 ? position + 1 : position - 1;
View replacement = getChildAt(newPosition);
int from = replacement.getLeft();
replacement.animate()
.setInterpolator(new DecelerateInterpolator())
.setDuration(getResources().getInteger(android.R.integer.config_mediumAnimTime))
.translationX(to - from)
.setListener(new SimpleAnimatorListener() {
#Override
public void onAnimationEnd(Animator animation) {
if (onViewRemovedListener != null) onViewRemovedListener.onRemoved(position, newPosition);
if (adapter!= null) adapter.notifyDataSetChanged();
}
})
.start();
}
}
}
public interface OnViewRemovedListener {
void onRemoved(int position, int newPosition);
}
I have a listview where pople can click on items. When they do, they increase height and show more information.
However, when clicking the bottom item, it is not visible to the user that the item height has increased and there is content if you scroll down.
Using e.g. following code in onItemClick does not solve the problem:
if (position == myItemsDataArrayList.size() - 1) {
if (data.ui_flags == "clicked") {
catalogListView.smoothScrollToPosition(position);
}
}
Implement this function in your if statement.
private void scrollMyListViewToBottom() {
myListView.post(new Runnable() {
#Override
public void run() {
// Select the last row so it will scroll into view...
myListView.setSelection(myListAdapter.getCount() - 1);
}
});
}
code by: https://stackoverflow.com/a/7032341/2197087
Working code solution was this:
catalogListView.post(new Runnable() {
#Override
public void run() {
catalogListView.setSelection(position_final);
}
});
I have a view that contains a ListView which is bound to a cursor adapter. When The cursor content change I want to keep the ListView at the top then in my custom cursor adapter I added :
#Override
protected void onContentChanged() {
// ...
myListView.scrollTo(0, 0);
}
but this doesn't work. Then I read somewhere to queue this action like this :
myListView.post(new Runnable() {
public void run() {
myListView.scrollTo(0, 0);
}
});
but this doesn't work either.
How can I keep the ListView at the top when its content changes?
EDIT:
Just for try, I added a button and called scrollTo() in its onClickListener and it didn't work! What am I missing ?
Instead of scrollTo, try setSelection(0) to get to the top position of list view.
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;
}
ListView's scrollTo applies to the ListView it self as a View
setSelection(0) does the trick because it applies to the listview's adapter
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;
}