GridLayoutManager.java revision b8403301bbec29129730f6cce3fe2fa3ee8e1e0b
1/*
2 * Copyright (C) 2014 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 languag`e governing permissions and
14 * limitations under the License.
15 */
16package android.support.v7.widget;
17
18import android.content.Context;
19import android.graphics.Rect;
20import android.os.Parcel;
21import android.os.Parcelable;
22import android.util.AttributeSet;
23import android.util.Log;
24import android.view.View;
25import android.view.ViewGroup;
26
27import java.util.Arrays;
28
29/**
30 * A {@link RecyclerView.LayoutManager} implementations that lays out items in a grid.
31 * <p>
32 * By default, each item occupies 1 span. You can change it by providing a custom
33 * {@link SpanSizeLookup} instance via {@link #setSpanSizeLookup(SpanSizeLookup)}.
34 */
35public class GridLayoutManager extends LinearLayoutManager {
36
37    private static final boolean DEBUG = false;
38    private static final String TAG = "GridLayoutManager";
39    public static final int DEFAULT_SPAN_COUNT = -1;
40    int mSpanCount = DEFAULT_SPAN_COUNT;
41    /**
42     * The size of each span
43     */
44    int mSizePerSpan;
45    /**
46     * Temporary array to keep views in layoutChunk method
47     */
48    View[] mSet;
49
50    /**
51     * The measure spec for the perpendicular orientation to {@link #getOrientation()}.
52     */
53    static int OTHER_DIM_SPEC = View.MeasureSpec.makeMeasureSpec(0, View.MeasureSpec.UNSPECIFIED);
54
55    SpanSizeLookup mSpanSizeLookup = new DefaultSpanSizeLookup();
56
57    public GridLayoutManager(Context context, int spanCount) {
58        super(context);
59        setSpanCount(spanCount);
60    }
61
62    public GridLayoutManager(Context context, int spanCount, int orientation,
63            boolean reverseLayout) {
64        super(context, orientation, reverseLayout);
65        setSpanCount(spanCount);
66    }
67
68    @Override
69    public void onLayoutChildren(RecyclerView.Recycler recycler, RecyclerView.State state) {
70        super.onLayoutChildren(recycler, state);
71        if (DEBUG) {
72            validateChildOrder();
73        }
74    }
75
76    @Override
77    public RecyclerView.LayoutParams generateDefaultLayoutParams() {
78        return new LayoutParams(ViewGroup.LayoutParams.WRAP_CONTENT,
79                ViewGroup.LayoutParams.WRAP_CONTENT);
80    }
81
82    @Override
83    public RecyclerView.LayoutParams generateLayoutParams(Context c, AttributeSet attrs) {
84        return new LayoutParams(c, attrs);
85    }
86
87    @Override
88    public RecyclerView.LayoutParams generateLayoutParams(ViewGroup.LayoutParams lp) {
89        if (lp instanceof ViewGroup.MarginLayoutParams) {
90            return new LayoutParams((ViewGroup.MarginLayoutParams) lp);
91        } else {
92            return new LayoutParams(lp);
93        }
94    }
95
96    @Override
97    public boolean checkLayoutParams(RecyclerView.LayoutParams lp) {
98        return lp instanceof LayoutParams;
99    }
100
101    /**
102     * Sets the source to get the number of spans occupied by each item in the adapter.
103     *
104     * @param spanSizeLookup {@link SpanSizeLookup} instance to be used to query number of spans
105     *                                             occupied by each item
106     */
107    public void setSpanSizeLookup(SpanSizeLookup spanSizeLookup) {
108        mSpanSizeLookup = spanSizeLookup;
109    }
110
111    /**
112     * Returns the current {@link SpanSizeLookup} used by the GridLayoutManager.
113     *
114     * @return The current {@link SpanSizeLookup} used by the GridLayoutManager.
115     */
116    public SpanSizeLookup getSpanSizeLookup() {
117        return mSpanSizeLookup;
118    }
119
120    private void updateMeasurements() {
121        int totalSpace;
122        if (getOrientation() == VERTICAL) {
123            totalSpace = getWidth() - getPaddingRight() - getPaddingLeft();
124        } else {
125            totalSpace = getHeight() - getPaddingBottom() - getPaddingTop();
126        }
127        mSizePerSpan = totalSpace / mSpanCount;
128    }
129
130    @Override
131    void onAnchorReady(LinearLayoutManager.AnchorInfo anchorInfo) {
132        super.onAnchorReady(anchorInfo);
133        updateMeasurements();
134        int span = mSpanSizeLookup.getSpanIndex(anchorInfo.mPosition, mSpanCount);
135        if (span != 0) { //this is not affected by RTL
136            if (DEBUG) {
137                Log.d(TAG, "Correcting span for position " + anchorInfo.mPosition);
138            }
139            int prev = anchorInfo.mPosition - 1;
140            while (span > 0) {
141                span -= mSpanSizeLookup.getSpanSize(prev);
142            }
143            anchorInfo.mPosition = prev;
144            if (DEBUG) {
145                Log.d(TAG, "corrected anchor position to " + anchorInfo.mPosition);
146            }
147        }
148        if (mSet == null || mSet.length != mSpanCount) {
149            mSet = new View[mSpanCount];
150        }
151    }
152
153    @Override
154    void layoutChunk(RecyclerView.Recycler recycler, RecyclerView.State state,
155            LayoutState layoutState, LayoutChunkResult result) {
156        int count = 0;
157        int remainingSpan = mSpanCount;
158        while (count < mSpanCount && layoutState.hasMore(state) && remainingSpan > 0) {
159            int pos = layoutState.mCurrentPosition;
160            final int spanSize = mSpanSizeLookup.getSpanSize(pos);
161            remainingSpan -= spanSize;
162            if (remainingSpan < 0) {
163                break; // item did not fit into this row or column
164            }
165            View view = layoutState.next(recycler);
166            if (view == null) {
167                break;
168            }
169            mSet[count] = view;
170            count ++;
171        }
172
173        if (count == 0) {
174            result.mFinished = true;
175            return;
176        }
177
178        int maxSize = 0;
179        final boolean layingOutInPrimaryDirection = mShouldReverseLayout ==
180                (layoutState.mLayoutDirection == LayoutState.LAYOUT_START);
181
182        for (int i = 0; i < count; i ++) {
183            View view = mSet[i];
184            if (layingOutInPrimaryDirection) {
185                addView(view);
186            } else {
187                addView(view, 0);
188            }
189            int spanSize = mSpanSizeLookup.getSpanSize(getPosition(view));
190            final int spec = View.MeasureSpec.makeMeasureSpec(mSizePerSpan * spanSize,
191                    View.MeasureSpec.EXACTLY);
192            if (mOrientation == VERTICAL) {
193                measureChildWithDecorationsAndMargin(view, spec, OTHER_DIM_SPEC);
194            } else {
195                measureChildWithDecorationsAndMargin(view, OTHER_DIM_SPEC, spec);
196            }
197            final int size = mOrientationHelper.getDecoratedMeasurement(view);
198            if (size > maxSize) {
199                maxSize = size;
200            }
201        }
202        result.mConsumed = maxSize;
203
204        int left = 0, top = 0, right = 0, bottom = 0;
205        if (mOrientation == VERTICAL) {
206            if (layoutState.mLayoutDirection == LayoutState.LAYOUT_START) {
207                bottom = layoutState.mOffset;
208                top = bottom - maxSize;
209            } else {
210                top = layoutState.mOffset;
211                bottom = top + maxSize;
212            }
213        } else {
214            if (layoutState.mLayoutDirection == LayoutState.LAYOUT_START) {
215                right = layoutState.mOffset;
216                left = right - maxSize;
217            } else {
218                left = layoutState.mOffset;
219                right = left + maxSize;
220            }
221        }
222        int span, spanDiff, start, end, diff;
223        // make sure we traverse from min position to max position
224        if (layingOutInPrimaryDirection) {
225            start = 0;
226            end = count;
227            diff = 1;
228        } else {
229            start = count - 1;
230            end = -1;
231            diff = -1;
232        }
233        if (mOrientation == VERTICAL && isLayoutRTL()) { // start from last span
234            span = mSpanCount - 1;
235            spanDiff = -1;
236        } else {
237            span = 0;
238            spanDiff = 1;
239        }
240        for (int i = start; i != end; i += diff) {
241            View view = mSet[i];
242            LayoutParams params = (LayoutParams) view.getLayoutParams();
243            int spanSize = mSpanSizeLookup.getSpanSize(getPosition(view));
244            final int startSpan;
245            if (spanDiff == -1 && spanSize > 1) {
246                startSpan = span - (spanSize - 1);
247            } else {
248                startSpan = span;
249            }
250            if (mOrientation == VERTICAL) {
251                left = getPaddingLeft() + mSizePerSpan * startSpan;
252                right = left + mOrientationHelper.getDecoratedMeasurementInOther(view);
253            } else {
254                top = getPaddingTop() + mSizePerSpan * startSpan;
255                bottom = top + mOrientationHelper.getDecoratedMeasurementInOther(view);
256            }
257            params.mSpanIndex = startSpan;
258            params.mSpanSize = spanSize;
259            // We calculate everything with View's bounding box (which includes decor and margins)
260            // To calculate correct layout position, we subtract margins.
261            layoutDecorated(view, left + params.leftMargin, top + params.topMargin,
262                    right - params.rightMargin, bottom - params.bottomMargin);
263            if (DEBUG) {
264                Log.d(TAG, "laid out child at position " + getPosition(view) + ", with l:"
265                        + (left + params.leftMargin) + ", t:" + (top + params.topMargin) + ", r:"
266                        + (right - params.rightMargin) + ", b:" + (bottom - params.bottomMargin)
267                        + ", span:" + span + ", spanSize:" + spanSize);
268            }
269            // Consume the available space if the view is not removed OR changed
270            if (params.isItemRemoved() || params.isItemChanged()) {
271                result.mIgnoreConsumed = true;
272            }
273            result.mFocusable |= view.isFocusable();
274            span += spanDiff * spanSize;
275        }
276        Arrays.fill(mSet, null);
277    }
278
279    private void measureChildWithDecorationsAndMargin(View child, int widthSpec, int heightSpec) {
280        final Rect insets = mRecyclerView.getItemDecorInsetsForChild(child);
281        RecyclerView.LayoutParams lp = (RecyclerView.LayoutParams) child.getLayoutParams();
282        widthSpec = updateSpecWithExtra(widthSpec, lp.leftMargin + insets.left,
283                lp.rightMargin + insets.right);
284        heightSpec = updateSpecWithExtra(heightSpec, lp.topMargin + insets.top,
285                lp.bottomMargin + insets.bottom);
286        child.measure(widthSpec, heightSpec);
287    }
288
289    private int updateSpecWithExtra(int spec, int startInset, int endInset) {
290        if (startInset == 0 && endInset == 0) {
291            return spec;
292        }
293        final int mode = View.MeasureSpec.getMode(spec);
294        if (mode == View.MeasureSpec.AT_MOST || mode == View.MeasureSpec.EXACTLY) {
295            return View.MeasureSpec.makeMeasureSpec(
296                    View.MeasureSpec.getSize(spec) - startInset - endInset, mode);
297        }
298        return spec;
299    }
300
301    /**
302     * Returns the number of spans laid out by this grid.
303     *
304     * @return The number of spans
305     * @see #setSpanCount(int)
306     */
307    public int getSpanCount() {
308        return mSpanCount;
309    }
310
311    /**
312     * Sets the number of spans to be laid out.
313     * <p>
314     * If {@link #getOrientation()} is {@link #VERTICAL}, this is the number of columns.
315     * If {@link #getOrientation()} is {@link #HORIZONTAL}, this is the number of rows.
316     *
317     * @param spanCount The total number of spans in the grid
318     * @see #getSpanCount()
319     */
320    public void setSpanCount(int spanCount) {
321        if (spanCount == mSpanCount) {
322            return;
323        }
324        if (spanCount < 1) {
325            throw new IllegalArgumentException("Span count should be at least 1. Provided "
326                    + spanCount);
327        }
328        mSpanCount = spanCount;
329    }
330
331    /**
332     * A helper class to provide the number of spans each item occupies.
333     * <p>
334     * Default implementation sets each item to occupy exactly 1 span.
335     *
336     * @see GridLayoutManager#setSpanSizeLookup(SpanSizeLookup)
337     */
338    public static abstract class SpanSizeLookup {
339        /**
340         * Returns the number of span occupied by the item at <code>position</code>.
341         *
342         * @param position The adapter position of the item
343         * @return The number of spans occupied by the item at the provided position
344         */
345        abstract public int getSpanSize(int position);
346
347        /**
348         * Returns the final span index of the provided position.
349         * <p>
350         * Default implementation traverses all items before the current position to decide which
351         * span offset this item should be positioned at. You can override this method if you have
352         * a faster way to calculate it based on your data set.
353         * <p>
354         * Note that span offsets always start with 0 and is not affected by RTL.
355         *
356         * @param position The position of the item
357         * @param spanCount The total number of spans in the grid
358         * @return The final span position of the item. Should be between 0 (inclusive) and
359         * <code>spanCount</code>(exclusive)
360         */
361        public int getSpanIndex(int position, int spanCount) {
362            int span = 0;
363            int positionSpanSize = getSpanSize(position);
364            if (positionSpanSize == spanCount) {
365                return 0; // quick return for full-span items
366            }
367            for (int i = 0; i < position; i++) {
368                int size = getSpanSize(position);
369                span += size;
370                if (span == spanCount) {
371                    span = 0;
372                } else if (span > spanCount) {
373                    // did not fit, moving to next row / column
374                    span = size;
375                }
376            }
377            if (span + positionSpanSize <= spanCount) {
378                return span;
379            }
380            return 0;
381        }
382    }
383
384    @Override
385    public boolean supportsPredictiveItemAnimations() {
386        return false;
387    }
388
389    /**
390     * Default implementation for {@link SpanSizeLookup}. Each item occupies 1 span.
391     */
392    public static final class DefaultSpanSizeLookup extends SpanSizeLookup {
393        @Override
394        public int getSpanSize(int position) {
395            return 1;
396        }
397
398        @Override
399        public int getSpanIndex(int position, int spanCount) {
400            return position % spanCount;
401        }
402    }
403
404    /**
405     * LayoutParams used by GridLayoutManager.
406     */
407    public static class LayoutParams extends RecyclerView.LayoutParams {
408
409        /**
410         * Span Id for Views that are not laid out yet.
411         */
412        public static final int INVALID_SPAN_ID = -1;
413
414        private int mSpanIndex = INVALID_SPAN_ID;
415
416        private int mSpanSize = 0;
417
418        public LayoutParams(Context c, AttributeSet attrs) {
419            super(c, attrs);
420        }
421
422        public LayoutParams(int width, int height) {
423            super(width, height);
424        }
425
426        public LayoutParams(ViewGroup.MarginLayoutParams source) {
427            super(source);
428        }
429
430        public LayoutParams(ViewGroup.LayoutParams source) {
431            super(source);
432        }
433
434        public LayoutParams(RecyclerView.LayoutParams source) {
435            super(source);
436        }
437
438        /**
439         * Returns the current span index of this View. If the View is not laid out yet, the return
440         * value is <code>undefined</code>.
441         * <p>
442         * Note that span index may change by whether the RecyclerView is RTL or not. For
443         * example, if the number of spans is 3 and layout is RTL, the rightmost item will have
444         * span index of 2. If the layout changes back to LTR, span index for this view will be 0.
445         * If the item was occupying 2 spans, span indices would be 1 and 0 respectively.
446         * <p>
447         * If the View occupies multiple spans, span with the minimum index is returned.
448         *
449         * @return The span index of the View.
450         */
451        public int getSpanIndex() {
452            return mSpanIndex;
453        }
454
455        /**
456         * Returns the number of spans occupied by this View. If the View not laid out yet, the
457         * return value is <code>undefined</code>.
458         *
459         * @return The number of spans occupied by this View.
460         */
461        public int getSpanSize() {
462            return mSpanSize;
463        }
464    }
465
466}
467