Java tutorial
/* * Copyright (c) 2017 Fondesa * * 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 com.fondesa.recyclerviewdivider; import android.annotation.SuppressLint; import android.content.Context; import android.graphics.Canvas; import android.graphics.Rect; import android.graphics.drawable.Drawable; import android.support.annotation.ColorInt; import android.support.annotation.IntDef; import android.support.annotation.NonNull; import android.support.annotation.Nullable; import android.support.v4.graphics.drawable.DrawableCompat; import android.support.v7.widget.RecyclerView; import android.util.Log; import android.view.View; import com.fondesa.recyclerviewdivider.factories.DrawableFactory; import com.fondesa.recyclerviewdivider.factories.MarginFactory; import com.fondesa.recyclerviewdivider.factories.SizeFactory; import com.fondesa.recyclerviewdivider.factories.TintFactory; import com.fondesa.recyclerviewdivider.factories.VisibilityFactory; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; /** * Class that draws a divider between RecyclerView's elements */ public class RecyclerViewDivider extends RecyclerView.ItemDecoration { private static final String TAG = "RecyclerViewDivider"; private static final int TYPE_SPACE = -1; private static final int TYPE_COLOR = 0; private static final int TYPE_DRAWABLE = 1; private final @Type int mType; private final VisibilityFactory mVisibilityFactory; private final DrawableFactory mDrawableFactory; private final TintFactory mTintFactory; private final SizeFactory mSizeFactory; private final MarginFactory mMarginFactory; /** * Set the {@link Builder} for this {@link RecyclerViewDivider} * * @param type divider's type (one of {@link Type}) * @param visibilityFactory instance of {@link VisibilityFactory} taken from {@link Builder} * @param drawableFactory instance of {@link DrawableFactory} taken from {@link Builder} * @param tintFactory instance of {@link TintFactory} taken from {@link Builder} * @param sizeFactory instance of {@link SizeFactory} taken from {@link Builder} * @param marginFactory instance of {@link MarginFactory} taken from {@link Builder} */ private RecyclerViewDivider(@Type int type, @NonNull VisibilityFactory visibilityFactory, @NonNull DrawableFactory drawableFactory, @Nullable TintFactory tintFactory, @NonNull SizeFactory sizeFactory, @NonNull MarginFactory marginFactory) { mType = type; mVisibilityFactory = visibilityFactory; mDrawableFactory = drawableFactory; mTintFactory = tintFactory; mSizeFactory = sizeFactory; mMarginFactory = marginFactory; } /** * Creates a new {@link Builder} for the current context * * @param context current context * @return a new {@link Builder} instance */ public static Builder with(@NonNull Context context) { return new Builder(context); } /** * Add this divider to a RecyclerView * * @param recyclerView RecyclerView at which the divider will be added */ public void addTo(@NonNull RecyclerView recyclerView) { removeFrom(recyclerView); recyclerView.addItemDecoration(this); } /** * Remove this divider from a RecyclerView * * @param recyclerView RecyclerView from which the divider will be removed */ public void removeFrom(@NonNull RecyclerView recyclerView) { recyclerView.removeItemDecoration(this); } @SuppressWarnings("ConstantConditions") @Override public void onDraw(Canvas c, RecyclerView parent, RecyclerView.State state) { final RecyclerView.Adapter adapter = parent.getAdapter(); final int listSize; // if the divider isn't a simple space, it will be drawn if (mType == TYPE_SPACE || adapter == null || (listSize = adapter.getItemCount()) == 0) return; int left; int top; int right; int bottom; final int orientation = RecyclerViewDividerUtils.getOrientation(parent); final int spanCount = RecyclerViewDividerUtils.getSpanCount(parent); int childCount = parent.getChildCount(); for (int i = 0; i < childCount; i++) { final View child = parent.getChildAt(i); int itemPosition = parent.getChildAdapterPosition(child); final int groupIndex = RecyclerViewDividerUtils.getGroupIndex(parent, itemPosition); final int groupCount = RecyclerViewDividerUtils.getGroupCount(parent, listSize); Drawable divider = mDrawableFactory.drawableForItem(groupCount, groupIndex); @VisibilityFactory.Show int showDivider = mVisibilityFactory.displayDividerForItem(groupCount, groupIndex); if (divider == null || showDivider == VisibilityFactory.SHOW_NONE) continue; final int spanSize = RecyclerViewDividerUtils.getSpanSize(parent, itemPosition); int lineAccumulatedSpan = RecyclerViewDividerUtils.getAccumulatedSpanInLine(parent, spanSize, itemPosition, groupIndex); final int margin = mMarginFactory.marginSizeForItem(groupCount, groupIndex); int size = mSizeFactory.sizeForItem(divider, orientation, groupCount, groupIndex); if (mTintFactory != null) { final int tint = mTintFactory.tintForItem(groupCount, groupIndex); Drawable wrappedDrawable = DrawableCompat.wrap(divider); DrawableCompat.setTint(wrappedDrawable, tint); divider = wrappedDrawable; } final RecyclerView.LayoutParams params = (RecyclerView.LayoutParams) child.getLayoutParams(); int halfSize = size < 2 ? size : size / 2; size = showDivider == VisibilityFactory.SHOW_ITEMS_ONLY ? 0 : size; halfSize = showDivider == VisibilityFactory.SHOW_GROUP_ONLY ? 0 : halfSize; final int childBottom = child.getBottom(); final int childTop = child.getTop(); final int childRight = child.getRight(); final int childLeft = child.getLeft(); // if the last element in the span doesn't complete the span count, its size will be full, not the half // halfSize * 2 is used instead of size to handle the case Show.ITEMS_ONLY in which size will be == 0 final int lastElementInSpanSize = itemPosition == listSize - 1 ? halfSize * 2 : halfSize; final boolean useCellMargin = margin == 0; int marginToAddBefore, marginToAddAfter; marginToAddBefore = marginToAddAfter = 0; if (orientation == RecyclerView.VERTICAL) { if (spanCount > 1 && spanSize < spanCount) { top = childTop + margin; // size is added to draw filling point between horizontal and vertical dividers bottom = childBottom - margin; if (useCellMargin) { if (groupIndex > 0) { top -= params.topMargin; } if (groupIndex < groupCount - 1 || size > 0) { bottom += params.bottomMargin; } bottom += size; } if (lineAccumulatedSpan == spanSize) { // first element in the group left = childRight + margin + params.rightMargin; right = left + lastElementInSpanSize; setBoundsAndDraw(divider, c, left, top, right, bottom); if (useCellMargin) { marginToAddAfter = params.rightMargin; } } else if (lineAccumulatedSpan == spanCount) { // last element in the group right = childLeft - margin - params.leftMargin; left = right - halfSize; setBoundsAndDraw(divider, c, left, top, right, bottom); if (useCellMargin) { marginToAddBefore = params.leftMargin; } } else { // element in the middle // left half divider right = childLeft - margin - params.leftMargin; left = right - halfSize; setBoundsAndDraw(divider, c, left, top, right, bottom); // right half divider left = childRight + margin + params.rightMargin; right = left + lastElementInSpanSize; setBoundsAndDraw(divider, c, left, top, right, bottom); if (useCellMargin) { marginToAddAfter = params.rightMargin; marginToAddBefore = params.leftMargin; } } } // draw bottom divider top = childBottom + params.bottomMargin; bottom = top + size; left = childLeft + margin - marginToAddBefore; right = childRight - margin + marginToAddAfter; setBoundsAndDraw(divider, c, left, top, right, bottom); } else { if (spanCount > 1 && spanSize < spanCount) { left = childLeft + margin; // size is added to draw filling point between horizontal and vertical dividers right = childRight - margin; if (useCellMargin) { if (groupIndex > 0) { left -= params.leftMargin; } if (groupIndex < groupCount - 1 || size > 0) { right += params.rightMargin; } right += size; } if (lineAccumulatedSpan == spanSize) { // first element in the group top = childBottom + margin + params.bottomMargin; bottom = top + lastElementInSpanSize; setBoundsAndDraw(divider, c, left, top, right, bottom); if (useCellMargin) { marginToAddAfter = params.bottomMargin; } } else if (lineAccumulatedSpan == spanCount) { // last element in the group bottom = childTop - margin - params.topMargin; top = bottom - halfSize; setBoundsAndDraw(divider, c, left, top, right, bottom); if (useCellMargin) { marginToAddBefore = params.topMargin; } } else { // element in the middle // top half divider bottom = childTop - margin - params.topMargin; top = bottom - halfSize; divider.setBounds(left, top, right, bottom); divider.draw(c); // bottom half divider top = childBottom + margin + params.bottomMargin; bottom = top + lastElementInSpanSize; setBoundsAndDraw(divider, c, left, top, right, bottom); if (useCellMargin) { marginToAddAfter = params.bottomMargin; marginToAddBefore = params.topMargin; } } } // draw right divider bottom = childBottom - margin + marginToAddAfter; top = childTop + margin - marginToAddBefore; left = childRight + params.rightMargin; right = left + size; setBoundsAndDraw(divider, c, left, top, right, bottom); } } } /** * Set the Drawable's bounds and draw it on a Canvas * * @param drawable Drawable to draw * @param canvas Canvas used to show the Drawable * @param left left position in px * @param top top position in px * @param right right position in px * @param bottom bottom position in px */ private void setBoundsAndDraw(@NonNull Drawable drawable, @NonNull Canvas canvas, int left, int top, int right, int bottom) { drawable.setBounds(left, top, right, bottom); drawable.draw(canvas); } @Override public void getItemOffsets(Rect outRect, View view, RecyclerView parent, RecyclerView.State state) { final int listSize = parent.getAdapter().getItemCount(); if (listSize <= 0) return; int itemPosition = parent.getChildAdapterPosition(view); final int groupIndex = RecyclerViewDividerUtils.getGroupIndex(parent, itemPosition); final int groupCount = RecyclerViewDividerUtils.getGroupCount(parent, listSize); @VisibilityFactory.Show int showDivider = mVisibilityFactory.displayDividerForItem(groupCount, groupIndex); if (showDivider == VisibilityFactory.SHOW_NONE) return; final int orientation = RecyclerViewDividerUtils.getOrientation(parent); final int spanCount = RecyclerViewDividerUtils.getSpanCount(parent); final int spanSize = RecyclerViewDividerUtils.getSpanSize(parent, itemPosition); int lineAccumulatedSpan = RecyclerViewDividerUtils.getAccumulatedSpanInLine(parent, spanSize, itemPosition, groupIndex); final Drawable divider = mDrawableFactory.drawableForItem(groupCount, groupIndex); int size = mSizeFactory.sizeForItem(divider, orientation, groupCount, groupIndex); int marginSize = mMarginFactory.marginSizeForItem(groupCount, groupIndex); int halfSize = size / 2 + marginSize; size = showDivider == VisibilityFactory.SHOW_ITEMS_ONLY ? 0 : size; halfSize = showDivider == VisibilityFactory.SHOW_GROUP_ONLY ? 0 : halfSize; if (orientation == RecyclerView.VERTICAL) { if (spanCount == 1 || spanSize == spanCount) { // LinearLayoutManager or GridLayoutManager with 1 column outRect.set(0, 0, 0, size); } else if (lineAccumulatedSpan == spanSize) { // first element in the group outRect.set(0, 0, halfSize, size); } else if (lineAccumulatedSpan == spanCount) { // last element in the group outRect.set(halfSize, 0, 0, size); } else { // element in the middle outRect.set(halfSize, 0, halfSize, size); } } else { if (spanCount == 1 || spanSize == spanCount) { // LinearLayoutManager or GridLayoutManager with 1 row outRect.set(0, 0, size, 0); } else if (lineAccumulatedSpan == spanSize) { // first element in the group outRect.set(0, 0, size, halfSize); } else if (lineAccumulatedSpan == spanCount) { // last element in the group outRect.set(0, halfSize, size, 0); } else { // element in the middle outRect.set(0, halfSize, size, halfSize); } } } /** * {@link Builder} class for {@link RecyclerViewDivider}. * <br> * This class can set these custom properties: * <ul> * <li><b>Color:</b> {@link #color(int)}</li> * <li><b>Drawable:</b> {@link #drawable(Drawable)}</li> * <li><b>Tint of the drawable:</b> {@link #tint(int)}</li> * <li><b>Size:</b> {@link #size(int)}</li> * <li><b>Margins:</b> {@link #marginSize(int)}</li> * </ul> * <br> * And use these custom factories: * <ul> * <li><b>{@link VisibilityFactory}:</b> {@link #visibilityFactory(VisibilityFactory)}</li> * <li><b>{@link DrawableFactory}:</b> {@link #drawableFactory(DrawableFactory)}</li> * <li><b>{@link TintFactory}:</b> {@link #tintFactory(TintFactory)}</li> * <li><b>{@link SizeFactory}:</b> {@link #sizeFactory(SizeFactory)}</li> * <li><b>{@link MarginFactory}:</b> {@link #marginFactory(MarginFactory)}</li> * </ul> */ public static class Builder { private static final int INT_DEF = -1; private final Context context; @ColorInt private Integer color; private Drawable drawable; private Integer tint; private int size; private int marginSize; private boolean hideLastDivider; private VisibilityFactory visibilityFactory; private DrawableFactory drawableFactory; private TintFactory tintFactory; private SizeFactory sizeFactory; private MarginFactory marginFactory; @Type private int type; /** * Initialize this {@link Builder} with a context. * The Context object will be stored in a WeakReference to avoid memory leak * * @param context current context */ @SuppressWarnings("WeakerAccess") public Builder(@NonNull Context context) { this.context = context; size = INT_DEF; marginSize = INT_DEF; type = TYPE_COLOR; } /** * Set the type of the divider as a space * * @return {@link Builder} instance */ public Builder asSpace() { type = TYPE_SPACE; return this; } /** * Set the color of all dividers. This method can't be used with {@link #drawable(Drawable)} or {@link #tint(int)} * <br> * To set a custom color for each divider use {@link #drawableFactory(DrawableFactory)} instead * * @param color resolved color for this divider, not a resource * @return {@link Builder} instance */ public Builder color(@ColorInt int color) { this.color = color; type = TYPE_COLOR; return this; } /** * Set the drawable of all dividers. This method can't be used with {@link #color(int)}. * If you want to color the drawable, you have to use {@link #tint(int)} instead. * <br> * To set a custom drawable for each divider use {@link #drawableFactory(DrawableFactory)} instead. * <br> * Warning: if the span count is major than one and the drawable can't be mirrored, the drawable will not be shown correctly. * * @param drawable custom drawable for this divider * @return {@link Builder} instance */ public Builder drawable(@NonNull Drawable drawable) { this.drawable = drawable; type = TYPE_DRAWABLE; return this; } /** * Set the tint color of all dividers' drawables. * If you want to create a plain divider with a single color, {@link #color(int)} is recommended. * <br> * To set a custom tint color for each divider's drawable use {@link #tintFactory(TintFactory)} instead * * @param color color that will be used as drawable's tint * @return {@link Builder} instance */ public Builder tint(@ColorInt int color) { tint = color; return this; } /** * Set the size of all dividers. The divider's final size will depend on RecyclerView's orientation: * <ul> * <li><b>RecyclerView.VERTICAL:</b> the height will be equal to the size and the width will be equal to the sum of container's width and the margin size</li> * <li><b>RecyclerView.HORIZONTAL:</b> the width will be equal to the size and the height will be equal to the sum of container's height and the margin size</li> * </ul> * <br> * To set a custom size for each divider use {@link #sizeFactory(SizeFactory)} instead. * * @param size size in pixels for this divider * @return {@link Builder} instance */ public Builder size(int size) { this.size = size; return this; } /** * Set the margin size for all dividers. They will depend on RecyclerView's orientation: * <ul> * <li><b>RecyclerView.VERTICAL:</b> margins will be added equally to the left and to the right</li> * <li><b>RecyclerView.HORIZONTAL:</b> margins will be added equally to the top and to the bottom</li> * </ul> * <br> * To set a custom margin size for each divider use {@link #sizeFactory(SizeFactory)} instead. * * @param marginSize margins' size in pixels for this divider * @return {@link Builder} instance */ public Builder marginSize(int marginSize) { this.marginSize = marginSize; return this; } /** * Hide the divider after the last group of items. * <br> * Warning: when the spanCount is major than 1 (e.g. LinearLayoutManager), only the divider after the last group will be hidden, the items' dividers, instead, will be shown. * <br> * If you want to specify a more flexible behaviour, use {@link #visibilityFactory(VisibilityFactory)} instead. * * @return {@link Builder} instance */ public Builder hideLastDivider() { this.hideLastDivider = true; return this; } /** * Set the divider's custom {@link VisibilityFactory} * <br> * If you want to hide only the last divider use {@link #hideLastDivider()} instead. * * @param visibilityFactory custom {@link VisibilityFactory} to set * @return {@link Builder} instance */ public Builder visibilityFactory(@Nullable VisibilityFactory visibilityFactory) { this.visibilityFactory = visibilityFactory; return this; } /** * Set the divider's custom {@link DrawableFactory} * <br> * Warning: if the span count is major than one and the drawable can't be mirrored, the drawable will not be shown correctly. * * @param drawableFactory custom {@link DrawableFactory} to set * @return {@link Builder} instance */ public Builder drawableFactory(@Nullable DrawableFactory drawableFactory) { this.drawableFactory = drawableFactory; return this; } /** * Set the divider's custom {@link TintFactory} * * @param tintFactory custom {@link TintFactory} to set * @return {@link Builder} instance */ public Builder tintFactory(@Nullable TintFactory tintFactory) { this.tintFactory = tintFactory; return this; } /** * Set the divider's custom {@link SizeFactory} * * @param sizeFactory custom {@link SizeFactory} to set * @return {@link Builder} instance */ public Builder sizeFactory(@Nullable SizeFactory sizeFactory) { this.sizeFactory = sizeFactory; return this; } /** * Set the divider's custom {@link MarginFactory} * * @param marginFactory custom {@link MarginFactory} to set * @return {@link Builder} instance */ public Builder marginFactory(@Nullable MarginFactory marginFactory) { this.marginFactory = marginFactory; return this; } /** * Creates a new {@link RecyclerViewDivider} with given configurations and initializes all values. * There are three common cases in the choice of factories: * <ul> * <li><b>Property not set</b>: the default factory will be used</li> * <li><b>Property set for all divider</b>: the general factory will be used</li> * <li><b>Property set differently for each divider</b>: the custom factory will be used</li> * </ul> * * @return a new {@link RecyclerViewDivider} with these {@link Builder} configurations */ @SuppressLint("SwitchIntDef") public RecyclerViewDivider build() { Log.d(TAG, "building the divider"); /* -------------------- VISIBILITY FACTORY -------------------- */ if (visibilityFactory == null) { if (hideLastDivider) { visibilityFactory = VisibilityFactory.getLastItemInvisibleFactory(); } else { visibilityFactory = VisibilityFactory.getDefault(); } } /* -------------------- SIZE FACTORY -------------------- */ if (sizeFactory == null) { if (size == INT_DEF) { sizeFactory = SizeFactory.getDefault(context); } else { sizeFactory = SizeFactory.getGeneralFactory(size); } } /* -------------------- DRAWABLE FACTORY -------------------- */ if (drawableFactory == null) { Drawable currDrawable = null; // all drawing properties will be set if RecyclerViewDivider is used as a divider, not as a space switch (type) { case TYPE_COLOR: if (color != null) { currDrawable = RecyclerViewDividerUtils.colorToDrawable(color); } break; case TYPE_DRAWABLE: if (drawable != null) { Log.d(TAG, "if your span count is major than 1 and the drawable can't be mirrored, it won't be shown correctly"); currDrawable = drawable; } break; } if (currDrawable == null) { drawableFactory = DrawableFactory.getDefault(context); } else { drawableFactory = DrawableFactory.getGeneralFactory(currDrawable); } } /* -------------------- TINT FACTORY -------------------- */ if (tintFactory == null) { if (tint != null) { tintFactory = TintFactory.getGeneralFactory(tint); } } /* -------------------- MARGIN FACTORY -------------------- */ if (marginFactory == null) { if (marginSize == INT_DEF) { marginFactory = MarginFactory.getDefault(context); } else { marginFactory = MarginFactory.getGeneralFactory(marginSize); } } // creates divider for this mBuilder return new RecyclerViewDivider(type, visibilityFactory, drawableFactory, tintFactory, sizeFactory, marginFactory); } } /** * Source annotation used to define different dividers' types. * <ul> * <li><b>TYPE_SPACE</b>: divider used only as a space</li> * <li><b>TYPE_COLOR</b>: plain divider with one color</li> * <li><b>TYPE_DRAWABLE</b>: divider with a drawable resource</li> * </ul> */ @IntDef({ TYPE_SPACE, TYPE_COLOR, TYPE_DRAWABLE }) @Retention(RetentionPolicy.SOURCE) private @interface Type { // empty annotation body } }