27

Google ハングアウトのチャット インターフェイスに似たインターフェイスを構築しています。新しいメッセージはリストの一番下に追加されます。リストの一番上までスクロールすると、以前のメッセージ履歴が読み込まれます。履歴がネットワークから受信されると、それらのメッセージがリストの一番上に追加され、ロードがトリガーされたときにユーザーが停止した位置から、いかなる種類のスクロールもトリガーされません。つまり、「読み込みインジケーター」がリストの上部に表示されます。

ここに画像の説明を入力

これは、ロードされた履歴にその場で置き換えられます。

ここに画像の説明を入力

私はこれをすべて機能させています... 達成するためにリフレクションに頼らなければならなかった1つのことを除いて. 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 の上部へのクイック フラッシュであり、次に正しい場所へのクイック フラッシュです。この問題はかなり明白で、多くの人が発見しsetSelection()います。そのため、ビューに描画の機会を与える必要があります。しかし、それはひどいようです。notifyDataSetChanged()ListViewpost()

リフレクションを使用して「修正」しました。私はそれが嫌いです。本質的に、私が達成したいのは、位置を設定した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かどうかだけお尋ねします。ListView

編集:Luksprogのコメント/回答に基づくリフレクションのない実用的なソリューション。

LuksprogOnPreDrawListener(). 魅力的な!以前に 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;
            }
        }
    });
}

とてもかっこいい。

4

3 に答える 3

18

コメントで述べたようにOnPreDrawlistener、問題を解決する別のオプションになる可能性があります。リスナーを使用するアイデアは、2 つの状態の間の表示をスキップすることですListView(データを追加した後、選択を正しい位置に設定した後)。OnPreDrawListener(set with )ではlistViewReference.getViewTreeObserver().addOnPreDrawListener(listener);、 の現在の表示位置を確認し、 が表示ListViewする位置に対してテストしListViewます。それらが一致しない場合は、リスナーのメソッドを戻しfalseてフレームをスキップし、選択範囲をListView正しい位置に設定します。適切な選択を設定すると、描画リスナーが再びトリガーされます。今回は位置が一致します。この場合、 の登録を解除してOnPreDrawlistenerを返しtrueます。

于 2013-10-17T09:32:58.707 に答える
0

質問の作者が提案したコードは機能しますが、危険です。
たとえば、この条件:

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-10-23T18:16:32.647 に答える