1/*
2 * Copyright 2018 The Android Open Source Project
3 *
4 * Licensed under the Apache License, Version 2.0 (the "License");
5 * you may not use this file except in compliance with the License.
6 * You may obtain a copy of the License at
7 *
8 *      http://www.apache.org/licenses/LICENSE-2.0
9 *
10 * Unless required by applicable law or agreed to in writing, software
11 * distributed under the License is distributed on an "AS IS" BASIS,
12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 * See the License for the specific language governing permissions and
14 * limitations under the License.
15 */
16package androidx.recyclerview.widget;
17
18import androidx.annotation.NonNull;
19
20/**
21 * Wraps a {@link ListUpdateCallback} callback and batches operations that can be merged.
22 * <p>
23 * For instance, when 2 add operations comes that adds 2 consecutive elements,
24 * BatchingListUpdateCallback merges them and calls the wrapped callback only once.
25 * <p>
26 * This is a general purpose class and is also used by
27 * {@link DiffUtil.DiffResult DiffResult} and
28 * {@link SortedList} to minimize the number of updates that are dispatched.
29 * <p>
30 * If you use this class to batch updates, you must call {@link #dispatchLastEvent()} when the
31 * stream of update events drain.
32 */
33public class BatchingListUpdateCallback implements ListUpdateCallback {
34    private static final int TYPE_NONE = 0;
35    private static final int TYPE_ADD = 1;
36    private static final int TYPE_REMOVE = 2;
37    private static final int TYPE_CHANGE = 3;
38
39    final ListUpdateCallback mWrapped;
40
41    int mLastEventType = TYPE_NONE;
42    int mLastEventPosition = -1;
43    int mLastEventCount = -1;
44    Object mLastEventPayload = null;
45
46    public BatchingListUpdateCallback(@NonNull ListUpdateCallback callback) {
47        mWrapped = callback;
48    }
49
50    /**
51     * BatchingListUpdateCallback holds onto the last event to see if it can be merged with the
52     * next one. When stream of events finish, you should call this method to dispatch the last
53     * event.
54     */
55    public void dispatchLastEvent() {
56        if (mLastEventType == TYPE_NONE) {
57            return;
58        }
59        switch (mLastEventType) {
60            case TYPE_ADD:
61                mWrapped.onInserted(mLastEventPosition, mLastEventCount);
62                break;
63            case TYPE_REMOVE:
64                mWrapped.onRemoved(mLastEventPosition, mLastEventCount);
65                break;
66            case TYPE_CHANGE:
67                mWrapped.onChanged(mLastEventPosition, mLastEventCount, mLastEventPayload);
68                break;
69        }
70        mLastEventPayload = null;
71        mLastEventType = TYPE_NONE;
72    }
73
74    @Override
75    public void onInserted(int position, int count) {
76        if (mLastEventType == TYPE_ADD && position >= mLastEventPosition
77                && position <= mLastEventPosition + mLastEventCount) {
78            mLastEventCount += count;
79            mLastEventPosition = Math.min(position, mLastEventPosition);
80            return;
81        }
82        dispatchLastEvent();
83        mLastEventPosition = position;
84        mLastEventCount = count;
85        mLastEventType = TYPE_ADD;
86    }
87
88    @Override
89    public void onRemoved(int position, int count) {
90        if (mLastEventType == TYPE_REMOVE && mLastEventPosition >= position &&
91                mLastEventPosition <= position + count) {
92            mLastEventCount += count;
93            mLastEventPosition = position;
94            return;
95        }
96        dispatchLastEvent();
97        mLastEventPosition = position;
98        mLastEventCount = count;
99        mLastEventType = TYPE_REMOVE;
100    }
101
102    @Override
103    public void onMoved(int fromPosition, int toPosition) {
104        dispatchLastEvent(); // moves are not merged
105        mWrapped.onMoved(fromPosition, toPosition);
106    }
107
108    @Override
109    public void onChanged(int position, int count, Object payload) {
110        if (mLastEventType == TYPE_CHANGE &&
111                !(position > mLastEventPosition + mLastEventCount
112                        || position + count < mLastEventPosition || mLastEventPayload != payload)) {
113            // take potential overlap into account
114            int previousEnd = mLastEventPosition + mLastEventCount;
115            mLastEventPosition = Math.min(position, mLastEventPosition);
116            mLastEventCount = Math.max(previousEnd, position + count) - mLastEventPosition;
117            return;
118        }
119        dispatchLastEvent();
120        mLastEventPosition = position;
121        mLastEventCount = count;
122        mLastEventPayload = payload;
123        mLastEventType = TYPE_CHANGE;
124    }
125}
126