I have 2 listview controls that each display part of the data of a list. They are oriented above and below each other in a UI fragment. The data displayed is tire information for a car, so the top listview displays data for the front wheels and the lower listview displays data for the rear wheels (with a car image between them). The listviews are configured so that the data list in each view mirrors the data in the other. So the top line of the top list is of the same dataset as the bottom line in the bottom list. When the user scrolls either list, the other list scrolls in a mirrored fashion. This enables the user to scroll the data such that the data they are interested in is positioned directly above and below the car image (bottom row of top list, top row of bottom list)
To accomplish this, I'm responding to the OnScrollListener-OnScroll event on one listview, making position calculations and calling listview.setSelectionFromTop(position, sety) on the other listview to update its position.
This is working fine in Android 4.xx when I first released the app. Now in Android 7 and higher, it appears that a race condition is occurring where setting the position on one listview is triggering an OnScroll on the other and things go bad from there in a never ending series of OnScroll events.
I've tried adding a flag on the OnScroll function so that it won't trigger if its inside an OnScroll call from the other listview. This didn't work as it appears that the OnScroll events triggered from calling setSelectionFromTop are asynchronous so they occur outside the flagged area.
One thing I've noticed is that it appears that the sizes of the listviews is critical and that they both need to be the exact same size. In the original layout (Android 4.x) I've set their layoutHeight=120dp to fix their size. However, current android doesn't appear to respect the layoutHeight values and the resulting UI shows them as different sizes, this may be a key to the puzzle.
Here's the onScroll code for one of the listviews, the code for the other is the same, just replace lvTop with lvBottom
OnScrollListener lTop=new OnScrollListener() {
@Override
public void onScrollStateChanged(AbsListView view, int scrollState) {
// TODO Auto-generated method stub
}
@Override
public void onScroll(AbsListView view, int firstVisibleItem,
int visibleItemCount, int totalItemCount) {
if (fInBottomScrollListener)
return;
fInTopScrollListener=true;
// Get position of this lv top
View cTop = lvTop.getChildAt(0);
View cBottom = lvBottom.getChildAt(0);
if (cTop==null || cBottom==null) return;
int topOffset = cTop.getTop();
int itemHeight = cTop.getHeight();
int firstVisPos = lvTop.getFirstVisiblePosition();
int setposition = totalItemCount - (visibleItemCount - (topOffset!=0 ? 1:0)) - firstVisPos-1;
int sety= -(itemHeight + (topOffset>0 ? 0:topOffset));
lvBottom.setSelectionFromTop(setposition, sety);
fInTopScrollListener=false;
}
};
I see in the log, a continuing list of onScroll events, when running on Android 4.X this list stops after scrolling is complete, whereas on 7.x and greater, it continues and scrolling is basically frozen on both listviews until I refresh the fragment
I think that your calculations for the position and offset are incorrect, so the 2 ListViews are bouncing beacuse of a small offset error.
For ListViews with same heights:
AbsListView.OnScrollListener lTop = new AbsListView.OnScrollListener() {
@Override
public void onScrollStateChanged(AbsListView view, int scrollState) {}
@Override
public void onScroll(AbsListView view, int firstVisibleItem,
int visibleItemCount, int totalItemCount) {
if (!fInBottomScrollListener) {
fInTopScrollListener = true;
View cTop = lvTop.getChildAt(visibleItemCount - 1);
if (cTop == null) return;
int setposition = totalItemCount - visibleItemCount - firstVisibleItem;
int sety = view.getHeight() - cTop.getBottom();
lvBottom.setSelectionFromTop(setposition, sety);
fInTopScrollListener = false;
}
}
};
AbsListView.OnScrollListener lBottom = new AbsListView.OnScrollListener() {
@Override
public void onScrollStateChanged(AbsListView view, int scrollState) {}
@Override
public void onScroll(AbsListView view, int firstVisibleItem,
int visibleItemCount, int totalItemCount) {
if (!fInTopScrollListener) {
fInBottomScrollListener = true;
View cBottom = lvBottom.getChildAt(visibleItemCount - 1);
if (cBottom == null) return;
int setposition = totalItemCount - visibleItemCount - firstVisibleItem;
int sety = view.getHeight() - cBottom.getBottom();
lvTop.setSelectionFromTop(setposition, sety);
fInBottomScrollListener = false;
}
}
};
Updated:
This is another approach, no matter the listviews' heights are same or not:
final private static int SCROLL_DELAY = 100;
long timestampScroll;
AbsListView.OnScrollListener lTop = new AbsListView.OnScrollListener() {
@Override
public void onScrollStateChanged(AbsListView view, int scrollState) {}
@Override
public void onScroll(AbsListView view, int firstVisibleItem,
int visibleItemCount, int totalItemCount) {
if (!fInBottomScrollListener) {
fInTopScrollListener = true;
View cTop = lvTop.getChildAt(0);
View cBot = lvBottom.getChildAt(0);
if (cTop == null || cBot == null) return;
int lvTopMovableRange = (cTop.getHeight() + lvTop.getDividerHeight()) * totalItemCount
- lvTop.getDividerHeight() - lvTop.getHeight();
int lvBotMovableRange = (cBot.getHeight() + lvBottom.getDividerHeight()) * totalItemCount
- lvBottom.getDividerHeight() - lvBottom.getHeight();
int lvTopPointer = (cTop.getHeight() + lvTop.getDividerHeight()) * firstVisibleItem
- cTop.getTop();
int lvBotPointer = (int)((float)(lvTopMovableRange - lvTopPointer)/lvTopMovableRange*lvBotMovableRange);
int setposition = lvBotPointer / (cBot.getHeight() + lvBottom.getDividerHeight());
int sety = -lvBotPointer + setposition * (cBot.getHeight() + lvBottom.getDividerHeight());
timestampScroll = System.currentTimeMillis();
lvBottom.setSelectionFromTop(setposition, sety);
lvTop.postDelayed(new Runnable() {
@Override
public void run() {
if (System.currentTimeMillis() - timestampScroll > (SCROLL_DELAY - 10)) fInTopScrollListener = false;
}
}, SCROLL_DELAY);
}
}
};
AbsListView.OnScrollListener lBottom = new AbsListView.OnScrollListener() {
@Override
public void onScrollStateChanged(AbsListView view, int scrollState) {}
@Override
public void onScroll(AbsListView view, int firstVisibleItem,
int visibleItemCount, int totalItemCount) {
if (!fInTopScrollListener) {
fInBottomScrollListener = true;
View cTop = lvTop.getChildAt(0);
View cBot = lvBottom.getChildAt(0);
if (cTop == null || cBot == null) return;
int lvTopMovableRange = (cTop.getHeight() + lvTop.getDividerHeight()) * totalItemCount
- lvTop.getDividerHeight() - lvTop.getHeight();
int lvBotMovableRange = (cBot.getHeight() + lvBottom.getDividerHeight()) * totalItemCount
- lvBottom.getDividerHeight() - lvBottom.getHeight();
int lvBotPointer = (cBot.getHeight() + lvBottom.getDividerHeight()) * firstVisibleItem
- cBot.getTop();
int lvTopPointer = (int)((float)(lvBotMovableRange - lvBotPointer)/lvBotMovableRange*lvTopMovableRange);
int setposition = lvTopPointer / (cTop.getHeight() + lvTop.getDividerHeight());
int sety = -lvTopPointer + setposition * (cTop.getHeight() + lvTop.getDividerHeight());
timestampScroll = System.currentTimeMillis();
lvTop.setSelectionFromTop(setposition, sety);
lvBottom.postDelayed(new Runnable() {
@Override
public void run() {
if (System.currentTimeMillis() - timestampScroll > (SCROLL_DELAY - 10)) fInBottomScrollListener = false;
}
}, SCROLL_DELAY);
}
}
};
Hope that helps!