eu.davidea.flexibleadapter.common.FlexibleItemDecoration.java Source code

Java tutorial

Introduction

Here is the source code for eu.davidea.flexibleadapter.common.FlexibleItemDecoration.java

Source

/*
 * Copyright 2016-2017
 * - The Android Open Source Project, for the drawing technique.
 * - Bo Song, for the item spacing technique.
 * - Davide Steduto, for the optimizations and all the rest.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 * http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package eu.davidea.flexibleadapter.common;

import android.annotation.SuppressLint;
import android.content.Context;
import android.content.res.TypedArray;
import android.graphics.Canvas;
import android.graphics.Rect;
import android.graphics.drawable.Drawable;
import android.support.annotation.DrawableRes;
import android.support.annotation.IntRange;
import android.support.annotation.LayoutRes;
import android.support.annotation.NonNull;
import android.support.v4.content.ContextCompat;
import android.support.v4.view.ViewCompat;
import android.support.v7.widget.GridLayoutManager;
import android.support.v7.widget.RecyclerView;
import android.support.v7.widget.StaggeredGridLayoutManager;
import android.util.SparseArray;
import android.view.View;

import eu.davidea.flexibleadapter.FlexibleAdapter;
import eu.davidea.flexibleadapter.items.IFlexible;
import eu.davidea.flexibleadapter.items.ISectionable;
import eu.davidea.flexibleadapter.utils.FlexibleUtils;

/**
 * This item decorator implements identical drawing technique of {@code DividerItemDecorator}
 * from Android API, and at the same time, it adds several useful functionalities:
 * <ul>
 * <li>Supports all RecyclerView LayoutManagers that implement (Linear, Grid or StaggeredGrid).</li>
 * <li>Can add <u>equal</u> space offset at all sides of an item.</li>
 * <li>Can customize the offset for each view type.</li>
 * <li>Can customize the offset at the 4 edges: first and last column / first and last row.</li>
 * <li>Recognizes the sections of FlexibleAdapter and can add gap offset between them.</li>
 * <li>Recognizes the orientation of the current Layout.</li>
 * <li>Supports the default Android divider {@code android.R.attr.listDivider}.</li>
 * <li>Supports a custom divider by {@code DrawableRes id}.</li>
 * <li>Supports drawing the divider over or underneath the items.</li>
 * </ul>
 * <b>Tip:</b> Call the method {@link FlexibleAdapter#invalidateItemDecorations(long)} to rebuild
 * the invalidated offsets due to the changes coming from events like moveItem or any layout
 * change that modifies the order of the items.
 *
 * @author Davide Steduto
 * @since 20/07/2015 Created
 * <br>23/04/2017 Drawing of the divider over or underneath the items
 * <br>26/05/2017 Rewrote the full class to support equal spaces between items in all situations
 */
@SuppressWarnings({ "WeakerAccess" })
public class FlexibleItemDecoration extends RecyclerView.ItemDecoration {

    private Context context;
    private final Rect mBounds = new Rect();
    private final ItemDecoration mDefaultDecoration = new ItemDecoration();
    private SparseArray<ItemDecoration> mDecorations; // viewType -> itemDeco

    private Drawable mDivider;
    private int mOffset, mSectionOffset;
    private boolean mDrawOver, withLeftEdge, withTopEdge, withRightEdge, withBottomEdge;

    private static final int[] ATTRS = new int[] { android.R.attr.listDivider };

    /**
     * Constructor which saves the Context to calculate the dpi OR to retrieve the divider later on.
     *
     * @param context current Context, it will be used to calculate dpi OR to retrieve the divider
     * @since 5.0.0-rc2
     */
    public FlexibleItemDecoration(@NonNull Context context) {
        this.context = context;
    }

    /**
     * Custom divider will be used.
     * <p>By default, divider will be drawn underneath the item.</p>
     *
     * @param context current context, it will be used to access resources
     * @param resId   drawable resourceId that should be used as a divider
     * @since 5.0.0-b4
     * @deprecated Unsupported. Use the methods {@code with...()} to configure the decoration
     */
    @Deprecated
    public FlexibleItemDecoration(@NonNull Context context, @DrawableRes int resId) {
        this(context, resId, 0);
    }

    /**
     * Custom divider with gap between sections (in dpi).
     * <p>An invalid divider ( {@code <= 0} ) resId, will be ignored!</p>
     *
     * @param context       current context, it will be used to access resources
     * @param resId         drawable resourceId that should be used as a divider
     * @param sectionOffset the extra offset at the end of each section
     * @since 5.0.0-b6
     * @deprecated Unsupported. Use the methods {@code with...()} to configure the decoration
     */
    @Deprecated
    public FlexibleItemDecoration(@NonNull Context context, @DrawableRes int resId,
            @IntRange(from = 0) int sectionOffset) {
        this.context = context;
        withDivider(resId);
        withSectionGapOffset(sectionOffset);
    }

    /*==========*/
    /* DIVIDERS */
    /*==========*/

    /**
     * Default Android divider will be used.
     *
     * @return this FlexibleItemDecoration instance so the call can be chained
     * @see #withDivider(int)
     * @see #withDrawOver(boolean)
     * @see #removeDivider()
     * @since 5.0.0-rc2
     */
    public FlexibleItemDecoration withDefaultDivider() {
        final TypedArray styledAttributes = context.obtainStyledAttributes(ATTRS);
        mDivider = styledAttributes.getDrawable(0);
        styledAttributes.recycle();
        return this;
    }

    /**
     * Custom divider.
     *
     * @param resId drawable resourceId that should be used as a divider
     * @return this FlexibleItemDecoration instance so the call can be chained
     * @see #withDefaultDivider()
     * @see #withDrawOver(boolean)
     * @see #removeDivider()
     * @since 5.0.0-rc2
     */
    public FlexibleItemDecoration withDivider(@DrawableRes int resId) {
        if (resId > 0)
            mDivider = ContextCompat.getDrawable(context, resId);
        return this;
    }

    /**
     * Removes any divider previously set.
     *
     * @return this FlexibleItemDecoration instance so the call can be chained
     * @since 5.0.0-rc2
     */
    public FlexibleItemDecoration removeDivider() {
        mDivider = null;
        return this;
    }

    /**
     * Changes the mode to draw the divider.
     * <p>- When {@code false}, any content will be drawn before the item views are drawn, and will
     * thus appear <i>underneath</i> the views.
     * <br>- When {@code true}, any content will be drawn after the item views are drawn, and will
     * thus appear <i>over</i> the views.</p>
     * Default value is false (drawn underneath).
     *
     * @param drawOver true to draw after the item has been added, false to draw underneath the item
     * @return this Divider, so the call can be chained
     * @since 5.0.0-b8
     */
    public FlexibleItemDecoration withDrawOver(boolean drawOver) {
        this.mDrawOver = drawOver;
        return this;
    }

    /**
     * @deprecated use {@link #withDrawOver(boolean)} instead.
     */
    @Deprecated
    public FlexibleItemDecoration setDrawOver(boolean drawOver) {
        return withDrawOver(drawOver);
    }

    @Override
    public void onDraw(Canvas c, RecyclerView parent, RecyclerView.State state) {
        if (mDivider != null && !mDrawOver) {
            draw(c, parent);
        }
    }

    @Override
    public void onDrawOver(Canvas c, RecyclerView parent, RecyclerView.State state) {
        if (mDivider != null && mDrawOver) {
            draw(c, parent);
        }
    }

    private void draw(Canvas c, RecyclerView parent) {
        if (parent.getLayoutManager() == null) {
            return;
        }
        if (FlexibleUtils.getOrientation(parent) == RecyclerView.VERTICAL) {
            drawVertical(c, parent);
        } else {
            drawHorizontal(c, parent);
        }
    }

    @SuppressLint("NewApi")
    private void drawVertical(Canvas canvas, RecyclerView parent) {
        canvas.save();
        final int left;
        final int right;
        if (parent.getClipToPadding()) {
            left = parent.getPaddingLeft();
            right = parent.getWidth() - parent.getPaddingRight();
            canvas.clipRect(left, parent.getPaddingTop(), right, parent.getHeight() - parent.getPaddingBottom());
        } else {
            left = 0;
            right = parent.getWidth();
        }

        final int itemCount = parent.getChildCount();
        for (int i = 0; i < itemCount - 1; i++) {
            final View child = parent.getChildAt(i);
            parent.getDecoratedBoundsWithMargins(child, mBounds);
            final int bottom = mBounds.bottom + Math.round(ViewCompat.getTranslationY(child));
            final int top = bottom - mDivider.getIntrinsicHeight();
            mDivider.setBounds(left, top, right, bottom);
            mDivider.draw(canvas);
        }
        canvas.restore();
    }

    @SuppressLint("NewApi")
    private void drawHorizontal(Canvas canvas, RecyclerView parent) {
        canvas.save();
        final int top;
        final int bottom;
        if (parent.getClipToPadding()) {
            top = parent.getPaddingTop();
            bottom = parent.getHeight() - parent.getPaddingBottom();
            canvas.clipRect(parent.getPaddingLeft(), top, parent.getWidth() - parent.getPaddingRight(), bottom);
        } else {
            top = 0;
            bottom = parent.getHeight();
        }

        final int itemCount = parent.getChildCount();
        for (int i = 0; i < itemCount - 1; i++) {
            final View child = parent.getChildAt(i);
            parent.getLayoutManager().getDecoratedBoundsWithMargins(child, mBounds);
            final int right = mBounds.right + Math.round(ViewCompat.getTranslationX(child));
            final int left = right - mDivider.getIntrinsicWidth();
            mDivider.setBounds(left, top, right, bottom);
            mDivider.draw(canvas);
        }
        canvas.restore();
    }

    /*==============================*/
    /* OFFSET & EDGES CONFIGURATION */
    /*==============================*/

    /**
     * @param gap offset gap between sections, in dpi. Must be positive.
     * @since 5.0.0-b6
     * @deprecated Use {@link #withSectionGapOffset(int)}
     */
    @Deprecated
    public void setSectionGapWidth(@IntRange(from = 0) int gap) {
        if (gap < 0) {
            throw new IllegalArgumentException("Invalid section gap width [<0]: " + gap);
        }
        mSectionOffset = gap;
    }

    /**
     * Adds an extra offset at the end of each section.
     * <p>Works only with {@link FlexibleAdapter}.</p>
     *
     * @param sectionOffset the extra offset at the end of each section
     * @return this FlexibleItemDecoration instance so the call can be chained
     * @since 5.0.0-rc2
     */
    public FlexibleItemDecoration withSectionGapOffset(@IntRange(from = 0) int sectionOffset) {
        mSectionOffset = (int) (context.getResources().getDisplayMetrics().density * sectionOffset);
        return this;
    }

    /**
     * Adds an offset to the items at the 4 edges (left, top, right and bottom).
     * <p><b>Note: </b>An offset OR an itemType with offsets must be added.</p>
     * Default value is {@code false} (no edge).
     *
     * @param withEdge true to enable, false otherwise
     * @return this FlexibleItemDecoration instance so the call can be chained
     * @see #withOffset(int)
     * @see #addItemViewType(int, int)
     * @since 5.0.0-rc2
     */
    public FlexibleItemDecoration withEdge(boolean withEdge) {
        withLeftEdge = withTopEdge = withRightEdge = withBottomEdge = withEdge;
        return this;
    }

    /**
     * Adds an offset to the items at the Left edge.
     * <p><b>Note: </b>An offset OR an itemType with offsets must be added.</p>
     * Default value is {@code false} (no edge).
     *
     * @param withLeftEdge true to add offset to the items at the Left of the first column.
     * @return this FlexibleItemDecoration instance so the call can be chained
     * @see #withOffset(int)
     * @see #addItemViewType(int, int)
     * @since 5.0.0-rc2
     */
    public FlexibleItemDecoration withLeftEdge(boolean withLeftEdge) {
        this.withLeftEdge = withLeftEdge;
        return this;
    }

    /**
     * Adds an offset to the items at the Top edge.
     * <p><b>Note: </b>An offset OR an itemType with offsets must be added.</p>
     * Default value is {@code false} (no edge).
     *
     * @param withTopEdge true to add offset to the items at the Top of the first row.
     * @return this FlexibleItemDecoration instance so the call can be chained
     * @see #withOffset(int)
     * @see #addItemViewType(int, int)
     * @since 5.0.0-rc2
     */
    public FlexibleItemDecoration withTopEdge(boolean withTopEdge) {
        this.withTopEdge = withTopEdge;
        return this;
    }

    /**
     * Adds an offset to the items at the Bottom edge.
     * <p><b>Note: </b>An offset OR an itemType with offsets must be added.</p>
     * Default value is {@code false} (no edge).
     *
     * @param withBottomEdge true to add offset to the items at the Bottom of the last row.
     * @return this FlexibleItemDecoration instance so the call can be chained
     * @see #withOffset(int)
     * @see #addItemViewType(int, int)
     * @since 5.0.0-rc2
     */
    public FlexibleItemDecoration withBottomEdge(boolean withBottomEdge) {
        this.withBottomEdge = withBottomEdge;
        return this;
    }

    /**
     * Adds an offset to the items at the Right edge.
     * <p><b>Note: </b>An offset OR an itemType with offsets must be added.</p>
     * Default value is {@code false} (no edge).
     *
     * @param withRightEdge true to add offset to the items at the Right of the last column.
     * @return this FlexibleItemDecoration instance so the call can be chained
     * @see #withOffset(int)
     * @see #addItemViewType(int, int)
     * @since 5.0.0-rc2
     */
    public FlexibleItemDecoration withRightEdge(boolean withRightEdge) {
        this.withRightEdge = withRightEdge;
        return this;
    }

    /**
     * Returns the current general offset in dpi.
     *
     * @return the offset previously set in dpi
     */
    public int getOffset() {
        return (int) (mOffset / context.getResources().getDisplayMetrics().density);
    }

    /**
     * Applies the physical offset between items, of the same size of the divider previously set.
     *
     * @param withOffset true to leave space between items, false divider will be drawn overlapping
     *                   the items
     * @return this FlexibleItemDecoration instance so the call can be chained
     * @since 5.0.0-b8
     * @deprecated Not applicable anymore, use the Divider or the new {@code #withOffset()} method
     */
    @Deprecated
    public FlexibleItemDecoration withOffset(boolean withOffset) {
        throw new UnsupportedOperationException(
                "withOffset(boolean) is unsupported, use the Divider or the new withOffset(int) method!");
    }

    /**
     * Applies the <u>same</u> physical offset to all sides of the item AND between items.
     *
     * @param offset the offset in dpi to apply
     * @return this FlexibleItemDecoration instance so the call can be chained
     * @since 5.0.0-rc2
     */
    public FlexibleItemDecoration withOffset(@IntRange(from = 0) int offset) {
        mOffset = (int) (context.getResources().getDisplayMetrics().density * offset);
        return this;
    }

    /**
     * Applies the general offset also to the specified viewType.
     * <p>Call {@link #withOffset(int)} to set a general offset.</p>
     *
     * @param viewType the viewType affected
     * @return this FlexibleItemDecoration instance so the call can be chained
     * @see #withOffset(int)
     * @see #addItemViewType(int, int)
     * @see #removeItemViewType(int)
     * @since 5.0.0-rc2
     */
    public FlexibleItemDecoration addItemViewType(@LayoutRes int viewType) {
        return addItemViewType(viewType, -1);
    }

    /**
     * As {@link #addItemViewType(int)} but with custom offset equals to all sides that will
     * affect only this viewType.
     *
     * @param viewType the viewType affected
     * @return this FlexibleItemDecoration instance so the call can be chained
     * @see #withOffset(int)
     * @see #removeItemViewType(int)
     * @since 5.0.0-rc2
     */
    public FlexibleItemDecoration addItemViewType(@LayoutRes int viewType, int offset) {
        return addItemViewType(viewType, offset, offset, offset, offset);
    }

    /**
     * As {@link #addItemViewType(int)} but with custom offset that will affect only this viewType.
     *
     * @param viewType the viewType affected
     * @param left     the offset to the left of the item
     * @param top      the offset to the top of the item
     * @param right    the offset to the right of the item
     * @param bottom   the offset to the bottom of the item
     * @return this FlexibleItemDecoration instance so the call can be chained
     * @see #addItemViewType(int, int)
     * @see #removeItemViewType(int)
     * @since 5.0.0-rc2
     */
    public FlexibleItemDecoration addItemViewType(@LayoutRes int viewType, int left, int top, int right,
            int bottom) {
        if (mDecorations == null) {
            mDecorations = new SparseArray<>();
        }
        left = (int) (context.getResources().getDisplayMetrics().density * left);
        top = (int) (context.getResources().getDisplayMetrics().density * top);
        right = (int) (context.getResources().getDisplayMetrics().density * right);
        bottom = (int) (context.getResources().getDisplayMetrics().density * bottom);
        mDecorations.put(viewType, new ItemDecoration(left, top, right, bottom));
        return this;
    }

    /**
     * In case a viewType should not have anymore the applied offset.
     *
     * @param viewType the viewType to remove from the decoration management
     * @return this FlexibleItemDecoration instance so the call can be chained
     * @since 5.0.0-rc2
     */
    public FlexibleItemDecoration removeItemViewType(@LayoutRes int viewType) {
        mDecorations.remove(viewType);
        return this;
    }

    /*====================*/
    /* OFFSET CALCULATION */
    /*====================*/

    /**
     * @since 5.0.0-rc2
     */
    @Override
    public void getItemOffsets(final Rect outRect, View view, RecyclerView recyclerView, RecyclerView.State state) {
        int position = recyclerView.getChildAdapterPosition(view);
        // Skip check so on item deleted, offset is kept (only if general offset was set!)
        // if (position == RecyclerView.NO_POSITION) return;

        // Get custom Item Decoration or default
        RecyclerView.Adapter adapter = recyclerView.getAdapter();
        int itemType = adapter.getItemViewType(position);
        ItemDecoration deco = getItemDecoration(itemType);

        // No offset set, applies the general offset to this item decoration
        if (!deco.hasOffset()) {
            deco = new ItemDecoration(mOffset);
        }

        // Default values (LinearLayout)
        int spanIndex = 0;
        int spanSize = 1;
        int spanCount = 1;
        int orientation = RecyclerView.VERTICAL;

        if (recyclerView.getLayoutManager() instanceof GridLayoutManager) {
            GridLayoutManager.LayoutParams lp = (GridLayoutManager.LayoutParams) view.getLayoutParams();
            spanIndex = lp.getSpanIndex();
            spanSize = lp.getSpanSize();
            GridLayoutManager lm = (GridLayoutManager) recyclerView.getLayoutManager();
            spanCount = lm.getSpanCount(); // Assume that there are spanCount items in this row/column.
            orientation = lm.getOrientation();

        } else if (recyclerView.getLayoutManager() instanceof StaggeredGridLayoutManager) {
            StaggeredGridLayoutManager.LayoutParams lp = (StaggeredGridLayoutManager.LayoutParams) view
                    .getLayoutParams();
            spanIndex = lp.getSpanIndex();
            StaggeredGridLayoutManager lm = (StaggeredGridLayoutManager) recyclerView.getLayoutManager();
            spanCount = lm.getSpanCount(); // Assume that there are spanCount items in this row/column.
            spanSize = lp.isFullSpan() ? spanCount : 1;
            orientation = lm.getOrientation();
        }

        boolean isFirstRowOrColumn = isFirstRowOrColumn(position, adapter, spanIndex, itemType);
        boolean isLastRowOrColumn = isLastRowOrColumn(position, adapter, spanIndex, spanCount, itemType);

        // Reset offset values
        int left = 0, top = 0, right = 0, bottom = 0;

        if (orientation == GridLayoutManager.VERTICAL) {
            int index = spanIndex;
            if (withLeftEdge)
                index = spanCount - spanIndex;
            left = deco.left * index / spanCount;

            index = (spanCount - (spanIndex + spanSize - 1) - 1);
            if (withRightEdge)
                index = spanIndex + spanSize;
            right = deco.right * index / spanCount;

            if (isFirstRowOrColumn && (withTopEdge)) {
                top = deco.top;
            }
            if (isLastRowOrColumn) {
                if (withBottomEdge) {
                    bottom = deco.bottom;
                }
            } else {
                bottom = deco.bottom;
            }

        } else {
            int index = spanIndex;
            if (withTopEdge)
                index = spanCount - spanIndex;
            top = deco.top * index / spanCount;

            index = (spanCount - (spanIndex + spanSize - 1) - 1);
            if (withBottomEdge)
                index = spanIndex + spanSize;
            bottom = deco.bottom * index / spanCount;

            if (isFirstRowOrColumn && (withLeftEdge)) {
                left = deco.left;
            }
            if (isLastRowOrColumn) {
                if (withRightEdge) {
                    right = deco.right;
                }
            } else {
                right = deco.right;
            }
        }

        outRect.set(left, top, right, bottom);

        applySectionGap(outRect, adapter, position, orientation);
    }

    @NonNull
    private ItemDecoration getItemDecoration(int itemType) {
        ItemDecoration deco = null;
        if (mDecorations != null) {
            deco = mDecorations.get(itemType);
        }
        if (deco == null) {
            deco = mDefaultDecoration;
        }
        return deco;
    }

    private boolean isFirstRowOrColumn(int position, RecyclerView.Adapter adapter, int spanIndex, int itemType) {
        int prePos = position > 0 ? position - 1 : -1;
        // Last position on the last row
        int preRowPos = position > spanIndex ? position - (1 + spanIndex) : -1;
        // isFirstRowOrColumn if one of the following condition is true
        return position == 0 || prePos == -1 || itemType != adapter.getItemViewType(prePos) || preRowPos == -1
                || itemType != adapter.getItemViewType(preRowPos);
    }

    private boolean isLastRowOrColumn(int position, RecyclerView.Adapter adapter, int spanIndex, int spanCount,
            int itemType) {
        int itemCount = adapter.getItemCount();
        int nextPos = position < itemCount - 1 ? position + 1 : -1;
        // First position on the next row
        int nextRowPos = position < itemCount - (spanCount - spanIndex) ? position + (spanCount - spanIndex) : -1;
        // isLastRowOrColumn if one of the following condition is true
        return position == itemCount - 1 || nextPos == -1 || itemType != adapter.getItemViewType(nextPos)
                || nextRowPos == -1 || itemType != adapter.getItemViewType(nextRowPos);
    }

    @SuppressWarnings("unchecked")
    private void applySectionGap(Rect outRect, RecyclerView.Adapter adapter, int position, int orientation) {
        // Section Gap Offset
        if (mSectionOffset > 0 && adapter instanceof FlexibleAdapter) {
            FlexibleAdapter flexibleAdapter = (FlexibleAdapter) adapter;
            IFlexible item = flexibleAdapter.getItem(position);

            // - Only ISectionable items can finish with a gap and only if next item is a IHeader item
            // - Important: the check must be done on the bottom of the section, otherwise the
            //   sticky header will jump!
            if (item instanceof ISectionable && (flexibleAdapter.isHeader(flexibleAdapter.getItem(position + 1))
                    || position >= adapter.getItemCount() - 1)) {

                if (orientation == RecyclerView.VERTICAL) {
                    outRect.bottom += mSectionOffset;
                } else {
                    outRect.right += mSectionOffset;
                }
            }
        }
    }

    private static class ItemDecoration {
        private int left, top, right, bottom;

        ItemDecoration() {
            this(-1);
        }

        ItemDecoration(int offset) {
            this(offset, offset, offset, offset);
        }

        ItemDecoration(int left, int top, int right, int bottom) {
            this.left = left;
            this.top = top;
            this.right = right;
            this.bottom = bottom;
        }

        final boolean hasOffset() {
            return this.top >= 0 || this.left >= 0 || this.right >= 0 || this.bottom >= 0;
        }
    }

}