DividerDecoration.java
/*
* Copyright (C) 2018 The Android Open Source Project
*
* 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 androidx.car.widget.itemdecorators;
import static androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX;
import android.content.Context;
import android.graphics.Canvas;
import android.graphics.Paint;
import android.graphics.Rect;
import android.view.View;
import androidx.annotation.ColorRes;
import androidx.annotation.IdRes;
import androidx.annotation.RestrictTo;
import androidx.car.R;
import androidx.car.util.GridLayoutManagerUtils;
import androidx.car.widget.PagedListView.DividerVisibilityManager;
import androidx.recyclerview.widget.GridLayoutManager;
import androidx.recyclerview.widget.RecyclerView;
/**
* A {@link RecyclerView.ItemDecoration} that will draw a dividing line between each item in the
* RecyclerView that it is added to.
*
* @hide
*/
@RestrictTo(LIBRARY_GROUP_PREFIX)
public class DividerDecoration extends RecyclerView.ItemDecoration {
public static final int INVALID_RESOURCE_ID = -1;
private final Context mContext;
private final Paint mPaint;
private final int mDividerHeight;
private final int mDividerStartMargin;
private final int mDividerEndMargin;
@IdRes private final int mDividerStartId;
@IdRes private final int mDividerEndId;
@ColorRes private int mListDividerColor;
private DividerVisibilityManager mVisibilityManager;
/**
* @param dividerStartMargin The start offset of the dividing line. This offset will be
* relative to {@code dividerStartId} if that value is given.
* @param dividerEndMargin The end offset of the dividing line. This offset will be
* relative to {@code dividerEndId} if that value is given.
* @param dividerStartId A child view id whose starting edge will be used as the starting
* edge of the dividing line. If this value is {@link #INVALID_RESOURCE_ID}, the top
* container of each child view will be used.
* @param dividerEndId A child view id whose ending edge will be used as the starting edge
* of the dividing line. If this value is {@link #INVALID_RESOURCE_ID}, then the top
* container view of each child will be used.
*/
public DividerDecoration(Context context, int dividerStartMargin,
int dividerEndMargin, @IdRes int dividerStartId, @IdRes int dividerEndId,
@ColorRes int listDividerColor) {
mContext = context;
mDividerStartMargin = dividerStartMargin;
mDividerEndMargin = dividerEndMargin;
mDividerStartId = dividerStartId;
mDividerEndId = dividerEndId;
mListDividerColor = listDividerColor;
mPaint = new Paint();
mPaint.setColor(mContext.getColor(listDividerColor));
mDividerHeight = mContext.getResources().getDimensionPixelSize(
R.dimen.car_list_divider_height);
}
/** Sets the color for the dividers. */
public void setDividerColor(@ColorRes int dividerColor) {
mListDividerColor = dividerColor;
updateDividerColor();
}
/** Updates the list divider color which may have changed due to a day night transition. */
public void updateDividerColor() {
mPaint.setColor(mContext.getColor(mListDividerColor));
}
/** Sets {@link DividerVisibilityManager} on the DividerDecoration.*/
public void setVisibilityManager(DividerVisibilityManager dvm) {
mVisibilityManager = dvm;
}
@Override
public void onDrawOver(Canvas c, RecyclerView parent, RecyclerView.State state) {
boolean usesGridLayoutManager = parent.getLayoutManager() instanceof GridLayoutManager;
for (int i = 0; i < parent.getChildCount(); i++) {
View container = parent.getChildAt(i);
int itemPosition = parent.getChildAdapterPosition(container);
if (!showDividerForAdapterPosition(itemPosition)) {
continue;
}
View nextVerticalContainer;
if (usesGridLayoutManager) {
// Find an item in next row to calculate vertical space.
int lastItemPosition =
GridLayoutManagerUtils.getLastItemPositionOnSameRow(container, parent);
nextVerticalContainer =
parent.getLayoutManager().findViewByPosition(lastItemPosition + 1);
} else {
nextVerticalContainer = parent.getChildAt(i + 1);
}
if (nextVerticalContainer == null) {
// Skip drawing divider for the last row in GridLayoutManager, or the last
// item (presumably in LinearLayoutManager).
continue;
}
int spacing = nextVerticalContainer.getTop() - container.getBottom();
// Sometimes during refresh, the nextVerticalContainer can still exist, but is
// not positioned in its corresponding position in the list (i.e. it has been pushed
// off-screen). This will result in a negative value for spacing. Do not draw a
// divider in this case to avoid the divider appearing in the wrong position.
if (spacing >= 0) {
drawDivider(c, container, spacing);
}
}
}
/**
* Draws a divider under {@code container}.
*
* @param spacing between {@code container} and next view.
*/
private void drawDivider(Canvas c, View container, int spacing) {
View startChild =
mDividerStartId != INVALID_RESOURCE_ID
? container.findViewById(mDividerStartId)
: container;
View endChild =
mDividerEndId != INVALID_RESOURCE_ID
? container.findViewById(mDividerEndId)
: container;
if (startChild == null || endChild == null) {
return;
}
Rect containerRect = new Rect();
container.getGlobalVisibleRect(containerRect);
Rect startRect = new Rect();
startChild.getGlobalVisibleRect(startRect);
Rect endRect = new Rect();
endChild.getGlobalVisibleRect(endRect);
int left = container.getLeft() + mDividerStartMargin
+ (startRect.left - containerRect.left);
int right = container.getRight() - mDividerEndMargin
- (endRect.right - containerRect.right);
// "(spacing + divider height) / 2" aligns the center of divider to that of spacing
// between two items.
// When spacing is an odd value (e.g. created by other decoration), space under divider
// is greater by 1dp.
int bottom = container.getBottom() + (spacing + mDividerHeight) / 2;
int top = bottom - mDividerHeight;
c.drawRect(left, top, right, bottom, mPaint);
}
@Override
public void getItemOffsets(Rect outRect, View view, RecyclerView parent,
RecyclerView.State state) {
super.getItemOffsets(outRect, view, parent, state);
int pos = parent.getChildAdapterPosition(view);
if (!showDividerForAdapterPosition(pos)) {
return;
}
// Add an bottom offset to all items that should have divider, even when divider is not
// drawn for the bottom item(s).
// With GridLayoutManager it's difficult to tell whether a view is in the last row.
// This is to keep expected behavior consistent.
outRect.bottom = mDividerHeight;
}
private boolean showDividerForAdapterPosition(int position) {
// If visibility manager is not set, default to show dividers.
return mVisibilityManager == null || mVisibilityManager.getShowDivider(position);
}
}