将项添加到ListView,保持滚动位置而不看滚动跳转

时间:2013-10-11 14:07:05

标签: android android-listview

我正在构建类似于Google Hangouts聊天界面的界面。新消息将添加到列表底部。向上滚动到列表顶部将触发先前消息历史记录的加载。当历史从网络进入时,这些消息将添加到列表的顶部,并且不应触发用户在触发负载时停止的位置的任何类型的滚动。换句话说,“加载指示符”显示在列表的顶部:

enter image description here

然后使用任何加载的历史记录替换它。

enter image description here

我所有这一切工作......除了一件我不得不求助于完成的事情。当向连接到ListView的适配器添加项目时,有很多问题和答案涉及仅保存和恢复滚动位置。我的问题是,当我做类似以下的事情时(简化但应该是不言自明的):

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的顶部,然后快速闪回到正确的位置。问题相当明显,发现by many peoplesetSelection()notifyDataSetChanged()之后和ListView的重绘之前不满意。所以我们必须post()给视图,让它有机会画画。 看起来很糟糕。

我通过使用反射来“修复”它。我讨厌它。在我的核心,我想要完成的是重置ListView的第一个位置,而不经过绘制周期的严格,直到之后我已经设置了位置。要做到这一点,ListView有一个有用的领域:mFirstPosition。通过gawd,这正是我需要调整的!不幸的是,它是包私有的。同样不幸的是,似乎没有任何方式以编程方式设置它或以任何不涉及无效循环的方式影响它......产生丑陋的行为。

所以,反思是失败后退:

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);
            }
        });
    }
}

有用吗?是。这很可怕吗?是。它将来会起作用吗?谁知道?有没有更好的办法? 这是我的问题。

如何在没有反思的情况下实现这一目标?

答案可能是“编写自己的ListView,可以处理此问题。”我只会问你是否seen the code for ListView

编辑:根据Luksprog的评论/回答没有反思的工作解决方案。

Luksprog推荐OnPreDrawListener()。迷人!我以前搞过ViewTreeObservers,但从来都不是其中之一。经过一番捣乱之后,以下类型的东西看起来非常完美。

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;
            }
        }
    });
}

非常酷。

3 个答案:

答案 0 :(得分:17)

正如我在评论中所说,OnPreDrawlistener可能是解决问题的另一种选择。使用侦听器的想法是跳过显示两个状态之间的ListView(在添加数据之后和将选择设置到正确位置之后)。在OnPreDrawListener(使用listViewReference.getViewTreeObserver().addOnPreDrawListener(listener);设置)中,您将检查ListView的当前可见位置,并根据ListView应显示的位置对其进行测试。如果那些不匹配,则使侦听器的方法返回false以跳过该帧并将ListView上的选择设置到正确的位置。设置正确的选择将再次触发绘图监听器,这次位置将匹配,在这种情况下,您将取消注册OnPreDrawlistener并返回true

答案 1 :(得分:4)

我一直在分手,直到找到类似的解决方案。 在添加一组项目之前,您必须保存firstVisible项目的顶部距离,并在添加项目后执行setSelectionFromTop()。

以下是代码:

// 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);

对我来说,它的工作没有任何跳跃,列出了大约500项:)

我从这篇SO帖子中获取了这段代码:Retaining position in ListView after calling notifyDataSetChanged

答案 2 :(得分:0)

问题作者提出的代码有效,但这很危险 例如,这个条件:

listView.getFirstVisiblePosition() == positionToSave
如果没有更改任何项目,

可能始终为真。

在当前元素的上方和下方添加了任意数量的元素的情况下,我遇到了一些问题。所以我提出了一个简单的改进版本:

/* 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);
        }
    });
}