【问题标题】:Adding items to ListView, maintaining scroll position and NOT seeing a scroll jump向 ListView 添加项目,保持滚动位置并且看不到滚动跳转
【发布时间】:2013-10-11 14:07:04
【问题描述】:

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

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

我已经完成了所有这些工作......除了我不得不诉诸反思来完成的一件事。在将项目添加到附加到 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。天哪,这正是我需要调整的!不幸的是,它是包私有的。同样不幸的是,似乎没有任何方法可以以编程方式设置它或以任何不涉及无效循环的方式影响它......产生丑陋的行为。

所以,以失败为后备的反思:

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

很酷。

【问题讨论】:

  • 我认为您可以尝试使用OnPreDrawListener。在该侦听器中,检查ListView 的当前第一个可见位置是否等于先前的第一个可见项目位置 + 添加的项目数,如果不是,则将ListView 上的选择设置为正确的位置并返回 false (跳过此帧)。如果位置匹配,则在同一个 OnPreDrawListener 中注销监听器(本身)并返回 true。
  • 有趣的想法!让我试一试这个想法……如果它成功了,请创建一个真正的答案并获得您的赏金。
  • Luksprog 发布你的答案,经过一些最终完美运行的骗局。好主意!
  • 可能这就是你要找的东西:stackoverflow.com/a/28149739/1468093

标签: android android-listview


【解决方案1】:

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

【讨论】:

  • 我的用户界面在执行此操作后被阻止了几秒钟,你知道吗?
  • @Arju 您是否有与问题完全相同的场景,或者您是否还添加了其他内容?
  • 我有一个列表视图必须滚动到的位置,然后我使用上面的代码,我认为它可以工作,但是 UI 被阻止
  • @arju 我不明白为什么会从我的代码中发生这种情况,它基本上会跳过绘制框架(因此时间将以毫秒为单位)。你是在监听器的else 子句中调用setSelection() 吗?
  • @Arju 抱歉,但我不明白为什么会发生阻塞。确保你没有做其他修改 ListView 并阻止它注册正确位置的事情。
【解决方案2】:

在找到类似的解决方案之前,我一直在崩溃。 在添加一组项目之前,您必须保存 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

【讨论】:

  • 这可能不是干净的代码,但它是这个线程中最好的解决方案。另一种解决方案强制列表在最后一个可见项目的确切位置重绘,这使得列表在第一个可见项目部分可见时“跳转”。此解决方案不会那样做。绝对是这里最好的。谢谢@BamsBamx
  • 如果你将项目添加到列表的底部,无论你怎么做,都不会跳转。但是,如果您在当前可见项目之上添加任何项目,则此答案中的代码将不起作用。
【解决方案3】:

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

listView.getFirstVisiblePosition() == positionToSave

如果没有更改任何项目,则可能始终为 true。

在当前元素上方和下方都添加了任意数量的元素的情况下,我在使用这种方法时遇到了一些问题。所以我想出了一个稍微改进的版本:

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

【讨论】:

    猜你喜欢
    • 2014-03-29
    • 2019-01-21
    • 1970-01-01
    • 1970-01-01
    • 2012-04-27
    • 1970-01-01
    • 2020-04-06
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多