I have a ListView with a set of children vertically listed (View objects) to be viewed by the users. I have to track the the user views, say,
a. If a user views a set of items for around 1 second, I should track the impressions.
b. If the same user scrolls the items out of the viewport and return back, I should track again, if he viewed for 1 second.
I tried several options like getGlobalVisibleRect(), getLocalVisibleRect(), getLocationOnScreen() and they are confusing in the first place and didn't help me get the right coordinates and visibility of the child items of the listView.
I checked Track impression of items in an android ListView which is a bit similar to my requirement but I thought to check if there is a better solution. I am new to Android and apologies if I am not clear on some explanations
To get your desired result, I think we have two different solutions. First, create Handler for each of the item and call / remove in scroll view if it is visible. But this is very much stupid one as creating so many Handlers will make your app's life hell.
Second and best way is to use call / remove a single Handler for the entire visible items. If it persist for a time "A second" (1 second for you), use impression count in each of your item's model class and increase it with ++ operator.
You can add scroll listener in your listivew. The script will be like-
ListView listView = null;
int firstVisibleItemIndex = 0;
int visibleCount = 0;
Handler handler = new Handler();
Runnable runnable = new Runnable() {
#Override
public void run() {
for (int i = firstVisibleItemIndex; i < firstVisibleItemIndex + visibleCount; i++) {
try {
//Get impression count from model for the visible item index i
int count = modelList.get(i).getImpressionCount();
//Set impression count to the model for the visible item index i
modelList.get(i).setImpressionCount(++count);
} catch (Exception e) {
e.printStackTrace();
}
}
}
};
//Can call this method body in onCreate directly
private void addListScrollListener() {
listView.setOnScrollListener(new AbsListView.OnScrollListener() {
#Override
public void onScroll(AbsListView view, int firstVisibleItem, int visibleItemCount, int totalItemCount) {
// You cat determine first and last visible items here
// final int lastVisibleItem = firstVisibleItem + visibleItemCount - 1;
handler.removeCallbacks(runnable);
firstVisibleItemIndex = firstVisibleItem;
visibleCount = visibleItemCount;
handler.postDelayed(runnable, 1000);
}
#Override
public void onScrollStateChanged(AbsListView arg0, int arg1) {
// TODO Auto-generated method stub
}
});
}
I assume that you will bind your ListView with the id in your onCreate method. Also you can call the listener thing in your onCreate after binding the view with the variable.
I hope this will work for your requirement.
Let me know your feedback.
Related
I'm developing a chat application and I'm trying to load 10 messages at a time, and when the user scrolls up it loads 10 more. therefore I used stackFromBottom=true to load the messages from the bottom :
<ListView
android:id="#+id/messagesList"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:transcriptMode="normal"
android:stackFromBottom="true">
And I have a scroll listener to know when the user reached the top (firstVisibleItem == 0 , i.e the first message) so I can load more messages:
messagesList.setOnScrollListener(new AbsListView.OnScrollListener() {
#Override
public void onScrollStateChanged(AbsListView absListView, int i) {
}
#Override
public void onScroll(AbsListView view, int firstVisibleItem, int visibleItemCount, int totalItemCount) {
if (firstVisibleItem ==0 && !loading && !startApp) {
loading = true;
skip += 10;
fetchMessages = new FetchMessages(); //load more items
fetchMessages.execute(true);
}
}
});
The issue is when you scroll up and reach the top, 10 more messages load but the Listview automatically scrolls to the bottom. I want to stay in the exact same position when new items are added to the Listview, just like any regular chat application.
This is the add messages method:
#Override
protected void onPostExecute(String json) {
if(json!=null){
...
//After Parse JSON ...
chatMessageAdapter.list.add(0,chatMessage); //add the message to the top of the list
chatMessageAdapter.notifyDataSetChanged(); //listview scrolls to the bottom afterwards
}
loading = false;
startApp = false;
}
I tried many of the suggestions in other threads but none of them actually worked. I hope someone could help here.
Many thanks.
what did work for me was that:
// save index and top position
int index = mList.getFirstVisiblePosition();
View v = mList.getChildAt(0);
int top = (v == null) ? 0 : v.getTop();
// notify dataset changed or re-assign adapter here
// restore the position of listview
mList.setSelectionFromTop(index, top);
Do note you have to remove android:transcriptMode="normal" from your xml, otherwise it wouldn't work.
This is my first ever post here and I'm a dumb novice, so I hope someone out there can both help me and excuse my ignorance.
I have a ListView which is populated with an ArrayAdapter. When I either scroll or click, I want the selected item, or the item nearest the vertical center, to be forced to the exact vertical center of the screen. If I call listView.setSelection(int position) it aligns the selected position at the top of the screen, so I need to use listView.setSelectionFromTop(position, offset) instead. To find my offset, I take half of the View's height from the half of the ListView's height.
So, I can vertically center my item easy enough, within OnItemClick or OnScrollStateChanged, with the following:
int x = listView.getHeight();
int y = listView.getChildAt(0).getHeight();
listView.setSelectionFromTop(myPosition, x/2 - y/2);
All this works fine. My problem is with the initial ListView setup. I want an item to be centered when the activity starts, but I can't because I get a NullPointerException from:
int y = listView.getChildAt(0).getHeight();
I understand this is because the ListView has not yet rendered, so it has no children, when I call this from OnCreate() or OnResume().
So my question is simply: how can I force my ListView to render at startup so I can get the height value I need? Or, alternatively, is there any other way to center items vertically within a ListView?
Thanks in advance for any help!
int y = listView.getChildAt(0).getHeight();
I understand this is because the ListView has not yet rendered, so it has no children, when I call this from onCreate() or onResume().
You should call it in onScroll.
listView.setOnScrollListener(new OnScrollListener() {
#Override
public void onScrollStateChanged(AbsListView view, int scrollState) {
}
#Override
public void onScroll(AbsListView view, int firstVisibleItem,
int visibleItemCount, int totalItemCount) {
//Write your logic here
int y = listView.getChildAt(0).getHeight();
}
});
I'm answering my own question here, but it's very much a hack. I think it's interesting because it sheds some light on the behavior of listviews.
The problem was in trying to act on data (a listview row) that did not yet exist (it had not been rendered). listview.getChildAt(int) was null because the listview had no children yet. I found out onScroll() is called immediately when the activity is created, so I simply put everything in a thread and delayed the getChildAt() call. I then enclosed the whole thing in a boolean wrapper to make sure it is only ever called once (on startup).
The interesting thing was that I only had to delay the call by 1ms for everything to be OK. And that's too fast for the eye to see.
Like I said, this is all a hack so I'm sure all this is a bad idea. Thanks for any help!
private boolean listViewReady = false;
public void onScroll(AbsListView view, int firstVisibleItem,
int visibleItemCount, int totalItemCount) {
if (!listViewReady){
Thread timer = new Thread() {
public void run() {
try{
sleep(1);
}catch (InterruptedException e) {
e.printStackTrace();
}finally{
runOnUiThread(new Runnable() {
public void run() {
myPosition = 2;
int x = listView.getHeight();
int y = listView.getChildAt(0).getHeight();
listView.setSelectionFromTop(myPosition, x/2 - y/2);
listViewReady = true;
}
});
}
}
};
timer.start();
}//if !ListViewReady
I have achieved the same using a in my opinion slighlty simpler solution
mListView.post(new Runnable() {
#Override
public void run() {
int height = mListView.getHeight();
int itemHeight = mListView.getChildAt(0).getHeight();
if (positionOfMyItem == myCollection.size() - 1) {
// last element - > don't subtract item height
itemHeight = 0;
}
mListView.setSelectionFromTop(position, height / 2 - itemHeight / 2);
}
});
Scope
I need to scroll to certain position smoothly and then "jump" to another position with setSelection(anotherPosition). This is done to create an illusion of smooth scrolling of (e.g.) 100 items in ListView. smoothScrollToPosition(100) lasts too much, you know.
Problem
setSelection() doesn't wait till smoothScrollToPosition finishes its work, so setSelection() is being called immediately and user sees quick jumping only;
Code
private final int scrollableItems = 20;
int firstVisiblePosition = mListView.getFirstVisiblePosition();
if (firstVisiblePosition < scrollableItems) {
mListView.smoothScrollToPosition(0);
} else {
mListView.smoothScrollToPosition(firstVisiblePosition - scrollableItems);
mListView.setSelection(0);
}
mListView.clearFocus();
Idea
OK, we could change logic of smoothness illusion: first setSelection(), then scroll smoothly (we're scrolling to the very first item on top of the list):
int firstVisiblePosition = mListView.getFirstVisiblePosition();
if (firstVisiblePosition < scrollableItems) {
mListView.smoothScrollToPosition(0);
} else {
mListView.setSelection(scrollableItems);
mListView.smoothScrollToPosition(0);
}
mListView.clearFocus();
final ListView listView = ...;
View listItemView = ...;
listView.smoothScrollBy(listItemView.getHeight() * NUMBER_OF_VIEWS,
DURATION * 2);
listView.postDelayed(new Runnable() {
public void run() {
listView.smoothScrollBy(0, 0); // Stops the listview from overshooting.
listView.setSelection(0);
}
}, DURATION);
Of course, direction of the scroll etc. would need to be adjusted for your use case (go to the top of the list)
EDIT: Old solution could overshoot if the velocity of the scroll was too high, smoothScrollBy(0,0) will stop the smooth scrolling before setting the selection properly and immediately.
Another way is to add an OnScrollListener.
private final int scrollableItems = 20;
int firstVisiblePosition = mListView.getFirstVisiblePosition();
if (firstVisiblePosition < scrollableItems) {
mListView.smoothScrollToPosition(0);
} else {
mListView.setOnScrollListener(new AbsListView.OnScrollListener() {
#Override
public void onScrollStateChanged(AbsListView absListView, int i) {
if (i == SCROLL_STATE_IDLE) {
mListView.setSelection(0);
}
}
})
mListView.smoothScrollToPosition(firstVisiblePosition - scrollableItems);
}
mListView.clearFocus();
I need to check all element on a ListView to set a label only to a one of those.
I can't edit the database or the adapter, I just want to scroll the ListView to perform a check and set a string on a TextView.
#Override
protected void onResume(){
super.onResume();
...
cursor = getCursor();
startManagingCursor(cursor);
adapter = new SimpleCursorAdapter(this,R.layout.profile_item_layout,cursor,from,to);
lst_profiles.setAdapter(adapter);
SharedPreferences customSharedPreference = PreferenceManager.getDefaultSharedPreferences(ProfilerActivity.this.getApplicationContext());
long current = customSharedPreference.getLong(ProfileManager.CURRENT, -1);
toast(lst_profiles.getChildCount()); //display 0
if(current!=-1){
for(int i=0;i<lst_profiles.getChildCount();i++){
if(lst_profiles.getItemIdAtPosition(i)==current)
((TextView)lst_profiles.getChildAt(i).findViewById(R.id.activeLabel)).setText(getString(R.string.active));
else
((TextView)lst_profiles.getChildAt(i).findViewById(R.id.activeLabel)).setText("");
}
}
}
How can I do? I need to wait something?
P.S. Obviusly the ListView is not empty.
That seems to be a nasty hack. But okay...
The thing is, that your list won't have children as long as the list is not displayed to the user. But you have to understand that getChildCount will return the amount of visible list items (so maybe about 10 views) and the position of them will never relate to the actual item position in the adapter.
If you really need to communicate with the views on a such low level you could try to attach a scroll listener to your list:
#Override
public void onScroll(AbsListView view, int firstVisibleItem, int visibleItemCount, int totalItemCount) {
for (int i = 0; i < visibleItemCount; i++) {
View child = getChildAt(i);
// i + firstVisibleItem == the actual item position in the adapter
}
}
I'm creating a list of pictures using a ListView and the photos are of a size that would fit 2 to 3 photos on the screen.
The problem that I'm having is that I would like to when the user stops scrolling that the first item of the visible list would snap to the top of screen, for example, if the scroll ends and small part of the first picture displayed, we scroll the list down so the picture is always fully displayed, if mostly of the picture is displayed, we scroll the list up so the next picture is fully visible.
Is there a way to achieve this in android with the listview?
I've found a way to do this just listening to scroll and change the position when the scroll ended by implementing ListView.OnScrollListener
#Override
public void onScrollStateChanged(AbsListView view, int scrollState) {
switch (scrollState) {
case OnScrollListener.SCROLL_STATE_IDLE:
if (scrolling){
// get first visible item
View itemView = view.getChildAt(0);
int top = Math.abs(itemView.getTop()); // top is a negative value
int bottom = Math.abs(itemView.getBottom());
if (top >= bottom){
((ListView)view).setSelectionFromTop(view.getFirstVisiblePosition()+1, 0);
} else {
((ListView)view).setSelectionFromTop(view.getFirstVisiblePosition(), 0);
}
}
scrolling = false;
break;
case OnScrollListener.SCROLL_STATE_TOUCH_SCROLL:
case OnScrollListener.SCROLL_STATE_FLING:
Log.i("TEST", "SCROLLING");
scrolling = true;
break;
}
}
The change is not so smooth but it works.
Utilizing a couple ideas from #nininho's solution, I got my listview to snap to the item with a smooth scroll instead of abruptly going to it. One caveat is that I've only tested this solution on a Moto X in a basic ListView with text, but it works very well on the device. Nevertheless, I'm confident about this solution, and encourage you to provide feedback.
listview.setOnScrollListener(new OnScrollListener() {
#Override
public void onScrollStateChanged(AbsListView view, int scrollState) {
// TODO Auto-generated method stub
if (scrollState == SCROLL_STATE_IDLE) {
View itemView = view.getChildAt(0);
int top = Math.abs(itemView.getTop());
int bottom = Math.abs(itemView.getBottom());
int scrollBy = top >= bottom ? bottom : -top;
if (scrollBy == 0) {
return;
}
smoothScrollDeferred(scrollBy, (ListView)view);
}
}
private void smoothScrollDeferred(final int scrollByF,
final ListView viewF) {
final Handler h = new Handler();
h.post(new Runnable() {
#Override
public void run() {
// TODO Auto-generated method stub
viewF.smoothScrollBy(scrollByF, 200);
}
});
}
#Override
public void onScroll(AbsListView view, int firstVisibleItem,
int visibleItemCount, int totalItemCount) {
// TODO Auto-generated method stub
}
});
The reason I defer the smooth scrolling is because in my testing, directly calling the smoothScrollBy method in the state changed callback had problems actually scrolling. Also, I don't foresee a fully-tested, robust solution holding very much state, and in my solution below, I hold no state at all. This solution is not yet in the Google Play Store, but should serve as a good starting point.
Using #nininho 's solution,
In the onScrollStateChanged when the state changes to SCROLL_STATE_IDLE, remember the position to snap and raise a flag:
snapTo = view.getFirstVisiblePosition();
shouldSnap = true;
Then, override the computeScroll() method:
#Override
public void computeScroll() {
super.computeScroll();
if(shouldSnap){
this.smoothScrollToPositionFromTop(snapTo, 0);
shouldSnap = false;
}
}
You can do a much more smooth scrolling if you use RecyclerView. The OnScrollListener is way better.
I have made an example here: https://github.com/plattysoft/SnappingList
Well.. I know 10 years have past since this question was asked, but now we can use LinearSnapHelper:
new LinearSnapHelper().attachToRecyclerView(recyclerView);
Source:
https://proandroiddev.com/android-recyclerview-snaphelper-19eaa9598da6
Apart from trying the code above one thing you should make sure of is that your listView have a height that can fit exact number of items you want to be displayed.
e.g
If you want 4 items to be displayed after snap effect and your row height (defined in its layout) should be 1/4 of the total height of the list.
Note that after the smoothScrollBy() call, getFirstVisiblePosition() may point to the list item ABOVE the topmost one in the listview. This is especially true when view.getChildAt(0).getBottom() == 0. I had to call view.setSelection(view.getFirstVisiblePosition() + 1) to remedy this odd behavior.