SmoothScrollToPositionFromTop() не всегда работает так, как должно
Я пытаюсь какое-то время работать smoothScrollToPositionFromTop(), но он не всегда перебирает правильную позицию.
У меня есть ListView (с 10 элементами) в макете с 10 кнопками сбоку, поэтому я могу прокручивать каждый элемент в списке. Обычно, когда я прокручиваю одну позицию назад или вперед, она работает нормально, но часто, когда я пытаюсь прокручивать более трех позиций назад или вперед, ListView точно не заканчивается в выбранной позиции. Когда он терпит неудачу, он обычно заканчивается от 0,5 до 1,5 элементов, и на самом деле он не предсказуем, когда прокрутка проваливается.
Я также проверил smoothScrollToPosition после того, как notifyDataSetChanged не работает в android, но это исправление не работает для меня, и я не изменяю никаких данных.
Мне бы очень хотелось автоматически прокручивать выбранные элементы списка, но не могу понять, как это сделать. У кого-нибудь была эта проблема раньше и знает, как ее исправить?
Ответы
Ответ 1
Это известная ошибка. См. https://code.google.com/p/android/issues/detail?id=36062
Однако я применил это обходное решение, которое касается всех случаев краев, которые могут возникнуть:
Сначала вызовите smothScrollToPositionFromTop(position)
, а затем, когда прокрутка завершена, вызовите setSelection(position)
. Последний вызов корректирует неполную прокрутку, перепрыгивая прямо в нужное положение. При этом у пользователя все еще создается впечатление, что он прокручивается в эту позицию.
Я реализовал это обходное решение в двух вспомогательных методах:
smoothScrollToPositionFromTop()
public static void smoothScrollToPositionFromTop(final AbsListView view, final int position) {
View child = getChildAtPosition(view, position);
// There no need to scroll if child is already at top or view is already scrolled to its end
if ((child != null) && ((child.getTop() == 0) || ((child.getTop() > 0) && !view.canScrollVertically(1)))) {
return;
}
view.setOnScrollListener(new AbsListView.OnScrollListener() {
@Override
public void onScrollStateChanged(final AbsListView view, final int scrollState) {
if (scrollState == SCROLL_STATE_IDLE) {
view.setOnScrollListener(null);
// Fix for scrolling bug
new Handler().post(new Runnable() {
@Override
public void run() {
view.setSelection(position);
}
});
}
}
@Override
public void onScroll(final AbsListView view, final int firstVisibleItem, final int visibleItemCount,
final int totalItemCount) { }
});
// Perform scrolling to position
new Handler().post(new Runnable() {
@Override
public void run() {
view.smoothScrollToPositionFromTop(position, 0);
}
});
}
getChildAtPosition()
public static View getChildAtPosition(final AdapterView view, final int position) {
final int index = position - view.getFirstVisiblePosition();
if ((index >= 0) && (index < view.getChildCount())) {
return view.getChildAt(index);
} else {
return null;
}
}
Ответ 2
Вот реализация решения.
void smoothScrollToPositionFromTopWithBugWorkAround(final AbsListView listView,
final int position,
final int offset,
final int duration){
//the bug workaround involves listening to when it has finished scrolling, and then
//firing a new scroll to the same position.
//the bug is the case that sometimes smooth Scroll To Position sort of misses its intended position.
//more info here : https://code.google.com/p/android/issues/detail?id=36062
listView.smoothScrollToPositionFromTop(position, offset, duration);
listView.setOnScrollListener(new OnScrollListener() {
@Override
public void onScrollStateChanged(AbsListView view, int scrollState) {
if(scrollState==OnScrollListener.SCROLL_STATE_IDLE){
listView.setOnScrollListener(null);
listView.smoothScrollToPositionFromTop(position, offset, duration);
}
}
@Override
public void onScroll(AbsListView view, int firstVisibleItem, int visibleItemCount, int totalItemCount) {
}
});
}
Ответ 3
Как упоминается на странице google issuetracker на четвертом этаже: https://issuetracker.google.com/issues/36952786
Обходной путь, описанный ранее: "Обходной путь на данный момент - это прослушивание SCROLL_STATE_IDLE при запуске свитка и smoothScrollToPositionFromTop снова в ту же позицию". не всегда будет работать.
Фактически, вызов onScrollStateChanged с помощью SCROLL_STATE_IDLE не обязательно означает, что прокрутка завершена. В результате он все еще не может гарантировать, что Listview каждый раз прокручивается до правильной позиции, особенно если представления элементов списка не одинаковы.
После исследования я нашел другой подход, который работает совершенно правильно и разумно. Как известно, Listview предоставляет метод scrollListBy (int y), который позволяет нам мгновенно прокручивать список List с y пикселями. Затем, с помощью таймера, мы можем прокручивать список плавно и правильно сами.
Первое, что нам нужно сделать, это вычислить высоту каждого представления элемента списка, включая представления за пределами экрана. Поскольку данные списка и типы дочерних представлений уже известны ранее, можно вычислить высоту каждого представления элемента списка. Таким образом, при заданной позиции для прокрутки до плавного, мы можем рассчитать его расстояние прокрутки в направлении y. Кроме того, расчет должен быть выполнен после завершения инициализации ListView.
Во-вторых, это сочетание таймера и метода scrollListBy (int). На самом деле мы можем использовать метод sendEmptyMessageDelayed() android.os.Handler. Таким образом, решение может быть:
/**
* Created by CaiHaozhong on 2017/9/29.
*/
public class ListViewSmoothScroller {
private final static int MSG_ACTION_SCROLL = 1;
private final static int MSG_ACTION_ADJUST = 2;
private ListView mListView = null;
/* The accumulated height of each list item view */
protected int[] mItemAccumulateHeight = null;
protected int mTimeStep = 20;
protected int mHeaderViewHeight;
private int mPos;
private Method mTrackMotionScrollMethod = null;
protected int mScrollUnit = 0;
protected int mTotalMove = 0;
protected int mTargetScrollDis = 0;
private Handler mMainHandler = new Handler(Looper.getMainLooper()){
public void handleMessage(Message msg) {
int what = msg.what;
switch (what){
case MSG_ACTION_SCROLL: {
int scrollDis = mScrollUnit;
if(mTotalMove + mScrollUnit > mTargetScrollDis){
scrollDis = mTargetScrollDis - mTotalMove;
}
if(Build.VERSION.SDK_INT >= 19) {
mListView.scrollListBy(scrollDis);
}
else{
if(mTrackMotionScrollMethod != null){
try {
mTrackMotionScrollMethod.invoke(mListView, -scrollDis, -scrollDis);
}catch(Exception ex){
ex.printStackTrace();
}
}
}
mTotalMove += scrollDis;
if(mTotalMove < mTargetScrollDis){
mMainHandler.sendEmptyMessageDelayed(MSG_ACTION_SCROLL, mTimeStep);
}else {
mMainHandler.sendEmptyMessageDelayed(MSG_ACTION_ADJUST, mTimeStep);
}
break;
}
case MSG_ACTION_ADJUST: {
mListView.setSelection(mPos);
break;
}
}
}
};
public ListViewSmoothScroller(Context context, ListView listView){
mListView = listView;
mScrollUnit = Tools.dip2px(context, 60);
mPos = -1;
try {
mTrackMotionScrollMethod = AbsListView.class.getDeclaredMethod("trackMotionScroll", int.class, int.class);
}catch (NoSuchMethodException ex){
ex.printStackTrace();
mTrackMotionScrollMethod = null;
}
if(mTrackMotionScrollMethod != null){
mTrackMotionScrollMethod.setAccessible(true);
}
}
/* scroll to a target position smoothly */
public void smoothScrollToPosition(int pos){
if(mListView == null)
return;
if(mItemAccumulateHeight == null || pos >= mItemAccumulateHeight.length){
return ;
}
mPos = pos;
mTargetScrollDis = mItemAccumulateHeight[pos];
mMainHandler.sendEmptyMessage(MSG_ACTION_SCROLL);
}
/* call after initializing ListView */
public void doMeasureOnLayoutChange(){
if(mListView == null){
return;
}
int headerCount = mListView.getHeaderViewsCount();
/* if no list item */
if(mListView.getChildCount() < headerCount + 1){
return ;
}
mHeaderViewHeight = 0;
for(int i = 0; i < headerCount; i++){
mHeaderViewHeight += mListView.getChildAt(i).getHeight();
}
View firstListItemView = mListView.getChildAt(headerCount);
computeAccumulateHeight(firstListItemView);
}
/* calculate the accumulated height of each list item */
protected void computeAccumulateHeight(View firstListItemView){
int len = listdata.size();// count of list item
mItemAccumulateHeight = new int[len + 2];
mItemAccumulateHeight[0] = 0;
mItemAccumulateHeight[1] = mHeaderViewHeight;
int currentHeight = mHeaderViewHeight;
for(int i = 2; i < len + 2; i++){
currentHeight += getItemHeight(firstListItemView);
mItemAccumulateHeight[i] = currentHeight;
}
}
/* get height of a list item. You may need to pass the listdata of the list item as parameter*/
protected int getItemHeight(View firstListItemView){
// Considering the structure of listitem View and the list data in order to calculate the height.
}
}
После завершения инициализации нашего ListView мы вызываем метод doMeasureOnLayoutChange(). После этого мы можем прокрутить ListView с помощью метода smoothScrollToPosition (int pos). Мы можем вызвать метод doMeasureOnLayoutChange() следующим образом:
mListAdapter.notifyDataSetChanged();
mListView.post(new Runnable() {
@Override
public void run() {
mListViewSmoothScroller.doMeasureOnLayoutChange();
}
});
Наконец, наш ListView можно прокручивать в целевую позицию плавно и, что более важно, правильно.