Android 将项目添加到ListView,保持滚动位置,并且看不到滚动跳转

Android 将项目添加到ListView,保持滚动位置,并且看不到滚动跳转,android,android-listview,Android,Android Listview,我正在构建一个类似于GoogleHangouts聊天界面的界面。新消息将添加到列表的底部。向上滚动到列表顶部将触发加载以前的消息历史记录。当历史记录来自网络时,这些消息会被添加到列表的顶部,并且不会从触发加载时用户停止的位置触发任何类型的滚动。换句话说,“加载指示器”显示在列表顶部: 然后将其替换为任何加载的历史记录 我有所有这些工作。。。除了一件我不得不通过反思来完成的事情。在向连接到ListView的适配器添加项目时,有很多问题和答案只涉及保存和恢复滚动位置。我的问题是,当我做以下事情时

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

然后将其替换为任何加载的历史记录

我有所有这些工作。。。除了一件我不得不通过反思来完成的事情。在向连接到ListView的适配器添加项目时,有很多问题和答案只涉及保存和恢复滚动位置。我的问题是,当我做以下事情时(简化但应不言自明):

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

我如何做到这一点而不反思?

答案可能是“编写您自己的
ListView
,它可以处理这个问题。”我只想问您是否已经完成了

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

建议使用
onpredrawstener()
。迷人的!我以前使用过ViewTreeObserver,但从未使用过。经过一番周折之后,下面这类事情似乎运行得相当完美

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;
            }
        }
    });
}
public void addNewItems(列表项){
final int positionToSave=listView.getFirstVisiblePosition();
adapter.addAll(项目);
post(新的Runnable(){
@凌驾
公开募捐{
listView.setSelection(位置保存);
}
});
listView.getViewTreeObserver().addOnPreDrawListener(新的OnPreDrawListener()){
@凌驾
公共布尔onPreDraw(){
if(listView.getFirstVisiblePosition()==positionToSave){
listView.getViewTreeObserver().removeOnPreDrawListener(此);
返回true;
}
否则{
返回false;
}
}
});
}

非常酷。

正如我在评论中所说,一个
onpredrawstener
可能是解决这个问题的另一个选择。使用侦听器的想法是跳过显示两种状态之间的
ListView
(添加数据后,将选择设置到正确位置后)。在
OnPreDrawListener
(使用
listViewReference.getViewTreeObserver().addOnPreDrawListener(listener);
设置)中,您将检查
ListView
的当前可见位置,并根据
ListView
应显示的位置对其进行测试。如果这些不匹配,则使侦听器的方法返回
false
跳过该帧,并将
列表视图上的选择设置到正确的位置。设置正确的选择将再次触发绘图侦听器,这一次位置将匹配,在这种情况下,您需要注销
OnPreDrawlistener
,并返回
true
,在找到类似的解决方案之前,我一直在绞尽脑汁。 在添加一组项目之前,必须保存第一个可见项目的顶部距离,在添加项目之后,执行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帖子中获取了这段代码:

问题作者建议的代码有效,但很危险。
例如,此条件:

listView.getFirstVisiblePosition() == positionToSave
如果未更改任何项目,则可能始终为真

在当前元素的上方和下方添加了任意数量的元素的情况下,我对这个aproach有一些问题。因此,我提出了一个略微改进的版本:

/* 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);
        }
    });
}
/*此侦听器将阻止调用的任何listView重画utils unlock()*/
私有类ListViewPredrawListener实现OnPreDrawListener{
私人视野;
私有布尔锁;
私有ListViewPredrawListener(视图){
this.view=视图;
}
公共无效锁(){
如果(!已锁定){
锁定=真;
view.getViewTreeObserver().addOnPreDrawListener(此);
}
}
公开作废解锁(){
如果(已锁定){
锁定=错误;
view.getViewTreeObserver().removeOnPreDrawListener(此);
}
}
@凌驾
公共布尔onPreDraw(){
返回false;
}
}
/*方法在我们的BaseAdapter中*/
私有更新列表(列出新项){
int pos=listView.getFirstVisiblePosition();
查看单元格=listView.getChildAt(pos);
String savedId=adapter.getItemId(pos);//用户当前正在查看的项目
savedPositionOffset=cell==null?0:cell.getTop();//当前项顶部偏移量
//现在我们阻止listView绘图,直到调用setSelectionFromTop()之后
最终ListViewPredrawListener predrawListener=新ListViewPredrawListener(listView);
predrawstener.lock();
//我们不知道项目和新项目之间发生了什么变化,这是唯一的假设
//我们要做的是,带有savedId的项仍然在newItems列表中
项目=新项目;
notifyDataSetChanged();
//或对于ArrayAdapter:
//清除();
//addAll(新项目);
post(新的Runnable(){
@凌驾
公开募捐{
//现在我们终于可以解锁listView图形了
//请注意,此代码将始终执行
predrawstener.unlock();
int newPosition=…;//基于savedId计算新位置
listView.setSelectionFromTop(新建位置、保存位置偏移);
}
});
}

我想您可以尝试使用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);
        }
    });
}