如何使用 recyclerview (SpannableGridLayoutManager) 设计可跨网格视图 [英] How to design spannable gridview using recyclerview (SpannableGridLayoutManager)
问题描述
我想设计如下图所示的网格视图.第一个项目应该比其他项目大.
目前我正在使用 RelativeLayout
和 GridLayoutManager
检查下面的代码
RecyclerView recyclerView = (RecyclerView)findViewById(R.id.recycler_view1);RecyclerView.LayoutManager recyclerViewLayoutManager = newGridLayoutManager(上下文,3);recyclerView.setLayoutManager(recyclerViewLayoutManager);recyclerView_Adapter = new RecyclerViewAdapter(context,numbers);recyclerView.setAdapter(recyclerView_Adapter);
用于演示的虚拟数组
String[] numbers = {一",二",三",四",五",六",七",八",九",十",十一",};
适配器类
公共类 RecyclerViewAdapter 扩展RecyclerView.Adapter{字符串[] 值;上下文上下文1;公共 RecyclerViewAdapter(Context context2,String[] values2){值 = 值 2;上下文 1 = 上下文 2;}公共静态类 ViewHolder 扩展 RecyclerView.ViewHolder{公共文本视图文本视图;公共 ViewHolder(View v){超级(五);textView = (TextView) v.findViewById(R.id.textview1);}}@覆盖public RecyclerViewAdapter.ViewHolder onCreateViewHolder(ViewGroup parent, int viewType){查看 view1 = LayoutInflater.from(context1).inflate(R.layout.recycler_view_items,parent,false);ViewHolder viewHolder1 = new ViewHolder(view1);返回 viewHolder1;}@覆盖public void onBindViewHolder(ViewHolder Vholder, int position){Vholder.textView.setText(values[position]);Vholder.textView.setBackgroundColor(Color.CYAN);Vholder.textView.setTextColor(Color.BLUE);}@覆盖公共 int getItemCount(){返回值.长度;} }
我已经为此实现了 SpannableGridLayoutManager
并且它对我来说完美无缺.请检查以下解决方案.
活动代码
SpannableGridLayoutManager gridLayoutManager = newSpannableGridLayoutManager(新的 SpannableGridLayoutManager.GridSpanLookup(){@覆盖公共 SpannableGridLayoutManager.SpanInfo getSpanInfo(int position){如果(位置== 0){返回新的 SpannableGridLayoutManager.SpanInfo(2, 2);//这将计算要替换的行和列} 别的 {返回新的 SpannableGridLayoutManager.SpanInfo(1, 1);}}}, 3, 1f);//3是列数,怎么显示是1frecyclerView.setLayoutManager(gridLayoutManager);
<块引用>
在适配器中编写以下代码
public MyViewHolder(View itemView) {超级(项目视图);GridLayoutManager.LayoutParams layoutParams = 新GridLayoutManager.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT,ViewGroup.LayoutParams.MATCH_PARENT);浮动边距 = DimensionUtils.convertDpToPixel(5);layoutParams.setMargins((int) margin, (int) margin, (int) margin,(int) 保证金);itemView.setLayoutParams(layoutParams);}
<块引用>
SpannableGridLayoutManager
自定义类
public class SpannableGridLayoutManager extends RecyclerView.LayoutManager {私人 GridSpanLookup spanLookup;私有整数列 = 1;私人浮动 cellAspectRatio = 1f;私人 int cellHeight;私有 int[] 单元格边框;私人 int firstVisiblePosition;私人 int lastVisiblePosition;私人 int firstVisibleRow;私人 int lastVisibleRow;私有布尔 forceClearOffsets;private SparseArray<GridCell>细胞;私人列表<整数>firstChildPositionForRow;//key == row, val == 第一个子位置私人 int totalRows;私人最终 Rect itemDecorationInsets = new Rect();公共 SpannableGridLayoutManager(GridSpanLookup spanLookup,int 列,浮动 cellAspectRatio){this.spanLookup = spanLookup;this.columns = 列;this.cellAspectRatio = cellAspectRatio;设置自动测量启用(真);}@Keep/* XML 构造函数,参见 RecyclerView#createLayoutManager */公共 SpannableGridLayoutManager(上下文上下文,AttributeSet attrs,int defStyleAttr,int defStyleRes){TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.SpannableGridLayoutManager, defStyleAttr, defStyleRes);列 = a.getInt(R.styleable.SpannableGridLayoutManager_android_orientation, 1);parseAspectRatio(a.getString(R.styleable.SpannableGridLayoutManager_aspectRatio));intorientation = a.getInt(R.styleable.SpannableGridLayoutManager_android_orientation, RecyclerView.VERTICAL);a.recycle();设置自动测量启用(真);}公共接口 GridSpanLookup {SpanInfo getSpanInfo(int position);}公共无效 setSpanLookup(@NonNull GridSpanLookup spanLookup) {this.spanLookup = spanLookup;}公共静态类 SpanInfo {公共 int columnSpan;公共 int rowSpan;公共 SpanInfo(int columnSpan, int rowSpan) {this.columnSpan = columnSpan;this.rowSpan = rowSpan;}public static final SpanInfo SINGLE_CELL = new SpanInfo(1, 1);}公共静态类 LayoutParams 扩展 RecyclerView.LayoutParams {整数列跨度;int rowSpan;公共布局参数(上下文 c,属性集属性){超级(c,属性);}公共布局参数(整数宽度,整数高度){超级(宽度,高度);}公共 LayoutParams(ViewGroup.MarginLayoutParams 源){超级(来源);}公共 LayoutParams(ViewGroup.LayoutParams 源){超级(来源);}公共布局参数(RecyclerView.LayoutParams 源){超级(来源);}}@覆盖public void onLayoutChildren(RecyclerView.Recycler recycler, RecyclerView.State state) {计算窗口大小();计算CellPositions(回收者,状态);如果 (state.getItemCount() == 0) {detachAndScrapAttachedViews(recycler);第一个可见行 = 0;resetVisibleItemTracking();返回;}//TODO 使用orientationHelperint startTop = getPaddingTop();int scrollOffset = 0;if (forceClearOffsets) {//见 #scrollToPositionstartTop = -(firstVisibleRow * cellHeight);forceClearOffsets = 假;} else if (getChildCount() != 0) {scrollOffset = getDecoratedTop(getChildAt(0));startTop = scrollOffset - (firstVisibleRow * cellHeight);resetVisibleItemTracking();}detachAndScrapAttachedViews(recycler);int row = firstVisibleRow;int availableSpace = getHeight() - scrollOffset;int lastItemPosition = state.getItemCount() - 1;while (availableSpace > 0 && lastVisiblePosition < lastItemPosition) {availableSpace -= layoutRow(row, startTop, recycler, state);row = getNextSpannedRow(row);}layoutDisappearingViews(recycler, state, startTop);}@覆盖公共 RecyclerView.LayoutParams generateDefaultLayoutParams() {返回新的 LayoutParams(ViewGroup.LayoutParams.WRAP_CONTENT,ViewGroup.LayoutParams.WRAP_CONTENT);}@覆盖公共 RecyclerView.LayoutParams generateLayoutParams(Context c, AttributeSet attrs) {返回新的 LayoutParams(c, attrs);}@覆盖公共 RecyclerView.LayoutParams generateLayoutParams(ViewGroup.LayoutParams lp) {if (lp instanceof ViewGroup.MarginLayoutParams) {返回新的 LayoutParams((ViewGroup.MarginLayoutParams) lp);} 别的 {返回新的布局参数(lp);}}@覆盖public boolean checkLayoutParams(RecyclerView.LayoutParams lp) {返回 LayoutParams 的 lp 实例;}@覆盖public void onAdapterChanged(RecyclerView.Adapter oldAdapter, RecyclerView.Adapter newAdapter) {removeAllViews();重启();}@覆盖public boolean supportsPredictiveItemAnimations() {返回真;}@覆盖公共布尔 canScrollVertically() {返回真;}@覆盖public int scrollVerticallyBy(int dy, RecyclerView.Recycler recycler, RecyclerView.State state) {if (getChildCount() == 0 || dy == 0) return 0;int滚动;int top = getDecoratedTop(getChildAt(0));if (dy < 0) {//向下滚动内容if (firstVisibleRow == 0) {//在内容的顶部int scrollRange = -(getPaddingTop() - top);滚动 = Math.max(dy, scrollRange);} 别的 {滚动 = dy;}if (top - scrolled >= 0) {//新的顶行出现在屏幕上int newRow = firstVisibleRow - 1;如果(新行> = 0){int startOffset = top - (firstVisibleRow * cellHeight);layoutRow(newRow, startOffset, recycler, state);}}int firstPositionOfLastRow = getFirstPositionInSpannedRow(lastVisibleRow);int lastRowTop = getDecoratedTop(getChildAt(firstPositionOfLastRow - firstVisiblePosition));if (lastRowTop - scrolled > getHeight()) {//最后一个跨行的行被滚出recycleRow(lastVisibleRow, recycler, state);}} else {//向上滚动内容int 底部 = getDecoratedBottom(getChildAt(getChildCount() - 1));if (lastVisiblePosition == getItemCount() - 1) {//在内容的结尾int scrollRange = Math.max(bottom - getHeight() + getPaddingBottom(), 0);滚动 = Math.min(dy, scrollRange);} 别的 {滚动 = dy;}if ((bottom - scrolled) < getHeight()) {//新行滚动进来int nextRow = lastVisibleRow + 1;如果 (nextRow < getSpannedRowCount()) {int startOffset = top - (firstVisibleRow * cellHeight);layoutRow(nextRow, startOffset, recycler, state);}}int lastPositionInRow = getLastPositionInSpannedRow(firstVisibleRow, state);int bottomOfFirstRow =getDecoratedBottom(getChildAt(lastPositionInRow - firstVisiblePosition));if (bottomOfFirstRow - scrolled < 0) {//第一个跨越的行被卷出recycleRow(firstVisibleRow, recycler, state);}}offsetChildrenVertical(-scrolled);返回滚动;}@覆盖公共无效滚动到位置(整数位置){if (position >= getItemCount()) position = getItemCount() - 1;firstVisibleRow = getRowIndex(position);resetVisibleItemTracking();forceClearOffsets = 真;removeAllViews();请求布局();}@覆盖public void smoothScrollToPosition(RecyclerView recyclerView, RecyclerView.State state, int position) {if (position >= getItemCount()) position = getItemCount() - 1;LinearSmoothScroller scroller = new LinearSmoothScroller(recyclerView.getContext()) {@覆盖公共 PointF computeScrollVectorForPosition(int targetPosition) {final int rowOffset = getRowIndex(targetPosition) - firstVisibleRow;返回新的 PointF(0, rowOffset * cellHeight);}};scroller.setTargetPosition(position);startSmoothScroll(scroller);}@覆盖public int computeVerticalScrollRange(RecyclerView.State state) {//TODO 更新它以增量计算返回 getSpannedRowCount() * cellHeight + getPaddingTop() + getPaddingBottom();}@覆盖公共 int computeVerticalScrollExtent(RecyclerView.State state) {返回 getHeight();}@覆盖public int computeVerticalScrollOffset(RecyclerView.State state) {如果 (getChildCount() == 0) 返回 0;return getPaddingTop() + (firstVisibleRow * cellHeight) - getDecoratedTop(getChildAt(0));}@覆盖公共视图 findViewByPosition(int position) {if (position < firstVisiblePosition || position > lastVisiblePosition) return null;返回 getChildAt(position - firstVisiblePosition);}公共 int getFirstVisibleItemPosition() {返回第一个可见位置;}私有静态类 GridCell {最后的 int 行;最终 int rowSpan;最后的 int 列;最终 int columnSpan;GridCell(int row, int rowSpan, int column, int columnSpan) {this.row = 行;this.rowSpan = rowSpan;this.column = 列;this.columnSpan = columnSpan;}}/*** 这是主要的布局算法,迭代所有项目并将它们放入[列,行]* 单元格位置.存储此布局信息以备后用.还记录了适配器位置* 每行开始于.* <p>* 注意,如果一行是跨行的,那么行开始位置记录为第一个单元格* 跨区单元格开始所在的行.这是为了确保我们有足够的连续* 布局/绘制跨行的视图.*/private void calculateCellPositions(RecyclerView.Recycler recycler, RecyclerView.State state) {final int itemCount = state.getItemCount();单元格 = 新的 SparseArray<>(itemCount);firstChildPositionForRow = new ArrayList<>();整数行 = 0;整数列 = 0;recordSpannedRowStartPosition(row, column);int[] rowHWM = new int[columns];//行高水位线(每列)for (int position = 0; position 列){spanInfo.columnSpan = 列;//还是应该抛出?}//检查当前位置的水平空间,否则开始新行//请注意,这可能会在网格中留下间隙;我们不会回溯去尝试适应//后续单元格进入间隙.我们将责任放在适配器上,以提供//连续数据,即不会跨越列边界以避免出现间隙.如果(列 + spanInfo.columnSpan > 列){行++;recordSpannedRowStartPosition(row, position);列 = 0;}//检查这个单元格是否已经被填充(通过前一个跨越单元格)而(rowHWM[列] > 行){列++;如果(列 + spanInfo.columnSpan > 列){行++;recordSpannedRowStartPosition(row, position);列 = 0;}}//此时,单元格应该适合[列,行]cell.put(position, new GridCell(row, spanInfo.rowSpan, column, spanInfo.columnSpan));//更新高水位记账for (int columnsSpanned = 0; columnsSpanned < spanInfo.columnSpan; columnsSpanned++) {rowHWM[column + columnsSpanned] = row + spanInfo.rowSpan;}//如果我们跨行,则将第一个子位置"记录为第一项//*在跨区项目开始的行中*.即该位置可能实际上并不坐//在行内,但它是我们需要渲染以填充的最早位置//请求的行.如果(spanInfo.rowSpan > 1){int rowStartPosition = getFirstPositionInSpannedRow(row);for (int rowsSpanned = 1;rowsSpanned < spanInfo.rowSpan;rowsSpanned++) {int spannedRow = 行 + 行跨行;recordSpannedRowStartPosition(spannedRow, rowStartPosition);}}//增加当前位置column += spanInfo.columnSpan;}totalRows = rowHWM[0];for (int i = 1; i < rowHWM.length; i++) {如果(rowHWM[i] > totalRows){totalRows = rowHWM[i];}}}私人 SpanInfo getSpanInfoFromAttachedView(int position) {for (int i = 0; i < getChildCount(); i++) {查看孩子 = getChildAt(i);如果(位置== getPosition(子)){LayoutParams lp = (LayoutParams) child.getLayoutParams();返回新的 SpanInfo(lp.columnSpan, lp.rowSpan);}}//错误?返回 SpanInfo.SINGLE_CELL;}private void recordSpannedRowStartPosition(final int rowIndex, final int position) {如果 (getSpannedRowCount() < (rowIndex + 1)) {firstChildPositionForRow.add(position);}}私有 int getRowIndex(最终的 int 位置){返回位置<单元格大小()?cell.get(position).row : -1;}私有 int getSpannedRowCount() {返回 firstChildPositionForRow.size();}私有 int getNextSpannedRow(int rowIndex) {int firstPositionInRow = getFirstPositionInSpannedRow(rowIndex);int nextRow = rowIndex + 1;while (nextRow < getSpannedRowCount()&&getFirstPositionInSpannedRow(nextRow) == firstPositionInRow) {nextRow++;}返回 nextRow;}私有 int getFirstPositionInSpannedRow(int rowIndex) {返回 firstChildPositionForRow.get(rowIndex);}private int getLastPositionInSpannedRow(final int rowIndex, RecyclerView.State state) {int nextRow = getNextSpannedRow(rowIndex);返回 (nextRow != getSpannedRowCount()) ?//检查是否到达边界getFirstPositionInSpannedRow(nextRow) - 1: state.getItemCount() - 1;}/*** 布置给定的行".如果请求的行包含,我们实际上可能会添加更多的行* 一个跨行的单元格.返回布局的行的像素高度.* <p>* 简化逻辑&簿记,视图按适配器顺序附加,即 child 0 将* 总是显示最早的位置等.*/私有 int layoutRow(int rowIndex, int startTop, RecyclerView.Recycler recycler, RecyclerView.State state) {int firstPositionInRow = getFirstPositionInSpannedRow(rowIndex);int lastPositionInRow = getLastPositionInSpannedRow(rowIndex, state);boolean containsRemovedItems = false;int insertPosition = (rowIndex < firstVisibleRow) ?0 : getChildCount();for (int position = firstPositionInRow;位置 <= lastPositionInRow;位置++,插入位置++) {查看视图 = recycler.getViewForPosition(position);LayoutParams lp = (LayoutParams) view.getLayoutParams();containsRemovedItems |= lp.isItemRemoved();GridCell 单元格 = cells.get(position);添加视图(视图,插入位置);//TODO 使用方向助手int wSpec = getChildMeasureSpec(cellBorders[cell.column + cell.columnSpan] - cellBorders[cell.column],View.MeasureSpec.EXACTLY, 0, lp.width, false);int hSpec = getChildMeasureSpec(cell.rowSpan * cellHeight,View.MeasureSpec.EXACTLY, 0, lp.height, true);measureChildWithDecorationsAndMargin(view, wSpec, hSpec);int left = cellBorders[cell.column] + lp.leftMargin;int top = startTop + (cell.row * cellHeight) + lp.topMargin;int right = left + getDecoratedMeasuredWidth(view);int 底部 = 顶部 + getDecoratedMeasuredHeight(view);layoutDecorated(视图,左,上,右,下);lp.columnSpan = cell.columnSpan;lp.rowSpan = cell.rowSpan;}如果(firstPositionInRow < firstVisiblePosition){firstVisiblePosition = firstPositionInRow;firstVisibleRow = getRowIndex(firstVisiblePosition);}如果(lastPositionInRow > lastVisiblePosition){lastVisiblePosition = lastPositionInRow;lastVisibleRow = getRowIndex(lastVisiblePosition);}if (containsRemovedItems) 返回 0;//不要为项目消失的行占用空间GridCell first = cells.get(firstPositionInRow);GridCell last = cells.get(lastPositionInRow);return (last.row + last.rowSpan - first.row) * cellHeight;}/*** 删除并回收此行"中的所有项目.如果该行包含跨行单元格,则所有* 跨行中的单元格将被删除.*/私有无效回收行(int rowIndex, RecyclerView.Recycler 回收器, RecyclerView.State 状态) {int firstPositionInRow = getFirstPositionInSpannedRow(rowIndex);int lastPositionInRow = getLastPositionInSpannedRow(rowIndex, state);int toRemove = lastPositionInRow;while (toRemove >= firstPositionInRow) {int index = toRemove - firstVisiblePosition;removeAndRecycleViewAt(index, recycler);去除 - ;}如果(rowIndex == firstVisibleRow){firstVisiblePosition = lastPositionInRow + 1;firstVisibleRow = getRowIndex(firstVisiblePosition);}如果(rowIndex == lastVisibleRow){lastVisiblePosition = firstPositionInRow - 1;lastVisibleRow = getRowIndex(lastVisiblePosition);}}private void layoutDisappearingViews(RecyclerView.Recycler 回收器,RecyclerView.State 状态,int startTop) {//去做}私有无效计算窗口大小(){//TODO 使用 OrientationHelper#getTotalSpaceint cellWidth =(int) Math.floor((getWidth() - getPaddingLeft() - getPaddingRight())/列);cellHeight = (int) Math.floor(cellWidth * (1f/cellAspectRatio));计算CellBorders();}私有无效重置(){单元格 = 空;firstChildPositionForRow = null;第一个可见位置 = 0;第一个可见行 = 0;lastVisiblePosition = 0;lastVisibleRow = 0;单元格高度 = 0;forceClearOffsets = 假;}私有无效重置VisibleItemTracking(){//保持 firstVisibleRow 但重置其他状态变量//TODO 使方向不可知int minimumVisibleRow = getMinimumFirstVisibleRow();if (firstVisibleRow > minimumVisibleRow) firstVisibleRow = minimumVisibleRow;firstVisiblePosition = getFirstPositionInSpannedRow(firstVisibleRow);lastVisibleRow = firstVisibleRow;lastVisiblePosition = firstVisiblePosition;}私有 int getMinimumFirstVisibleRow() {int maxDisplayedRows = (int) Math.ceil((float) getHeight()/cellHeight) + 1;if (totalRows 0 && denominatorValue > 0){cellAspectRatio = Math.abs(nominatorValue/denominatorValue);返回;}} catch (NumberFormatException e) {//忽略}}}}throw new IllegalArgumentException("无法解析纵横比:'" + aspect + "'");}}
<块引用>
在attr文件中添加以下代码
<attr name="android:orientation"/><attr name="spanCount"/><attr name="aspectRatio" format="string"/></declare-styleable>
I want to design grid view like below image provided. The first item should be bigger than the rest.
Currently I am using RelativeLayout
with GridLayoutManager
check below code
RecyclerView recyclerView = (RecyclerView)
findViewById(R.id.recycler_view1);
RecyclerView.LayoutManager recyclerViewLayoutManager = new
GridLayoutManager(context, 3);
recyclerView.setLayoutManager(recyclerViewLayoutManager);
recyclerView_Adapter = new RecyclerViewAdapter(context,numbers);
recyclerView.setAdapter(recyclerView_Adapter);
Dummy array for demo
String[] numbers = {
"one",
"two",
"three",
"four",
"five",
"six",
"seven",
"eight",
"nine",
"ten",
"eleven",
};
Adapter Class
public class RecyclerViewAdapter extends
RecyclerView.Adapter<RecyclerViewAdapter.ViewHolder>{
String[] values;
Context context1;
public RecyclerViewAdapter(Context context2,String[] values2){
values = values2;
context1 = context2;
}
public static class ViewHolder extends RecyclerView.ViewHolder{
public TextView textView;
public ViewHolder(View v){
super(v);
textView = (TextView) v.findViewById(R.id.textview1);
}
}
@Override
public RecyclerViewAdapter.ViewHolder onCreateViewHolder(ViewGroup parent, int viewType){
View view1 = LayoutInflater.from(context1).inflate(R.layout.recycler_view_items,parent,false);
ViewHolder viewHolder1 = new ViewHolder(view1);
return viewHolder1;
}
@Override
public void onBindViewHolder(ViewHolder Vholder, int position){
Vholder.textView.setText(values[position]);
Vholder.textView.setBackgroundColor(Color.CYAN);
Vholder.textView.setTextColor(Color.BLUE);
}
@Override
public int getItemCount(){
return values.length;
} }
I have implemented the SpannableGridLayoutManager
for this and its working perfectly for me. Please check the following solution.
Activity Code
SpannableGridLayoutManager gridLayoutManager = new
SpannableGridLayoutManager(new SpannableGridLayoutManager.GridSpanLookup() {
@Override
public SpannableGridLayoutManager.SpanInfo getSpanInfo(int position)
{
if (position == 0) {
return new SpannableGridLayoutManager.SpanInfo(2, 2);
//this will count of row and column you want to replace
} else {
return new SpannableGridLayoutManager.SpanInfo(1, 1);
}
}
}, 3, 1f); // 3 is the number of coloumn , how nay to display is 1f
recyclerView.setLayoutManager(gridLayoutManager);
In adapter write following Code
public MyViewHolder(View itemView) {
super(itemView);
GridLayoutManager.LayoutParams layoutParams = new
GridLayoutManager.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT,
ViewGroup.LayoutParams.MATCH_PARENT);
float margin = DimensionUtils.convertDpToPixel(5);
layoutParams.setMargins((int) margin, (int) margin, (int) margin,
(int) margin);
itemView.setLayoutParams(layoutParams);
}
SpannableGridLayoutManager
custom class
public class SpannableGridLayoutManager extends RecyclerView.LayoutManager {
private GridSpanLookup spanLookup;
private int columns = 1;
private float cellAspectRatio = 1f;
private int cellHeight;
private int[] cellBorders;
private int firstVisiblePosition;
private int lastVisiblePosition;
private int firstVisibleRow;
private int lastVisibleRow;
private boolean forceClearOffsets;
private SparseArray<GridCell> cells;
private List<Integer> firstChildPositionForRow; // key == row, val == first child position
private int totalRows;
private final Rect itemDecorationInsets = new Rect();
public SpannableGridLayoutManager(GridSpanLookup spanLookup, int columns, float cellAspectRatio) {
this.spanLookup = spanLookup;
this.columns = columns;
this.cellAspectRatio = cellAspectRatio;
setAutoMeasureEnabled(true);
}
@Keep /* XML constructor, see RecyclerView#createLayoutManager */
public SpannableGridLayoutManager(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.SpannableGridLayoutManager, defStyleAttr, defStyleRes);
columns = a.getInt(R.styleable.SpannableGridLayoutManager_android_orientation, 1);
parseAspectRatio(a.getString(R.styleable.SpannableGridLayoutManager_aspectRatio));
int orientation = a.getInt(R.styleable.SpannableGridLayoutManager_android_orientation, RecyclerView.VERTICAL);
a.recycle();
setAutoMeasureEnabled(true);
}
public interface GridSpanLookup {
SpanInfo getSpanInfo(int position);
}
public void setSpanLookup(@NonNull GridSpanLookup spanLookup) {
this.spanLookup = spanLookup;
}
public static class SpanInfo {
public int columnSpan;
public int rowSpan;
public SpanInfo(int columnSpan, int rowSpan) {
this.columnSpan = columnSpan;
this.rowSpan = rowSpan;
}
public static final SpanInfo SINGLE_CELL = new SpanInfo(1, 1);
}
public static class LayoutParams extends RecyclerView.LayoutParams {
int columnSpan;
int rowSpan;
public LayoutParams(Context c, AttributeSet attrs) {
super(c, attrs);
}
public LayoutParams(int width, int height) {
super(width, height);
}
public LayoutParams(ViewGroup.MarginLayoutParams source) {
super(source);
}
public LayoutParams(ViewGroup.LayoutParams source) {
super(source);
}
public LayoutParams(RecyclerView.LayoutParams source) {
super(source);
}
}
@Override
public void onLayoutChildren(RecyclerView.Recycler recycler, RecyclerView.State state) {
calculateWindowSize();
calculateCellPositions(recycler, state);
if (state.getItemCount() == 0) {
detachAndScrapAttachedViews(recycler);
firstVisibleRow = 0;
resetVisibleItemTracking();
return;
}
// TODO use orientationHelper
int startTop = getPaddingTop();
int scrollOffset = 0;
if (forceClearOffsets) { // see #scrollToPosition
startTop = -(firstVisibleRow * cellHeight);
forceClearOffsets = false;
} else if (getChildCount() != 0) {
scrollOffset = getDecoratedTop(getChildAt(0));
startTop = scrollOffset - (firstVisibleRow * cellHeight);
resetVisibleItemTracking();
}
detachAndScrapAttachedViews(recycler);
int row = firstVisibleRow;
int availableSpace = getHeight() - scrollOffset;
int lastItemPosition = state.getItemCount() - 1;
while (availableSpace > 0 && lastVisiblePosition < lastItemPosition) {
availableSpace -= layoutRow(row, startTop, recycler, state);
row = getNextSpannedRow(row);
}
layoutDisappearingViews(recycler, state, startTop);
}
@Override
public RecyclerView.LayoutParams generateDefaultLayoutParams() {
return new LayoutParams(
ViewGroup.LayoutParams.WRAP_CONTENT,
ViewGroup.LayoutParams.WRAP_CONTENT);
}
@Override
public RecyclerView.LayoutParams generateLayoutParams(Context c, AttributeSet attrs) {
return new LayoutParams(c, attrs);
}
@Override
public RecyclerView.LayoutParams generateLayoutParams(ViewGroup.LayoutParams lp) {
if (lp instanceof ViewGroup.MarginLayoutParams) {
return new LayoutParams((ViewGroup.MarginLayoutParams) lp);
} else {
return new LayoutParams(lp);
}
}
@Override
public boolean checkLayoutParams(RecyclerView.LayoutParams lp) {
return lp instanceof LayoutParams;
}
@Override
public void onAdapterChanged(RecyclerView.Adapter oldAdapter, RecyclerView.Adapter newAdapter) {
removeAllViews();
reset();
}
@Override
public boolean supportsPredictiveItemAnimations() {
return true;
}
@Override
public boolean canScrollVertically() {
return true;
}
@Override
public int scrollVerticallyBy(int dy, RecyclerView.Recycler recycler, RecyclerView.State state) {
if (getChildCount() == 0 || dy == 0) return 0;
int scrolled;
int top = getDecoratedTop(getChildAt(0));
if (dy < 0) { // scrolling content down
if (firstVisibleRow == 0) { // at top of content
int scrollRange = -(getPaddingTop() - top);
scrolled = Math.max(dy, scrollRange);
} else {
scrolled = dy;
}
if (top - scrolled >= 0) { // new top row came on screen
int newRow = firstVisibleRow - 1;
if (newRow >= 0) {
int startOffset = top - (firstVisibleRow * cellHeight);
layoutRow(newRow, startOffset, recycler, state);
}
}
int firstPositionOfLastRow = getFirstPositionInSpannedRow(lastVisibleRow);
int lastRowTop = getDecoratedTop(
getChildAt(firstPositionOfLastRow - firstVisiblePosition));
if (lastRowTop - scrolled > getHeight()) { // last spanned row scrolled out
recycleRow(lastVisibleRow, recycler, state);
}
} else { // scrolling content up
int bottom = getDecoratedBottom(getChildAt(getChildCount() - 1));
if (lastVisiblePosition == getItemCount() - 1) { // is at end of content
int scrollRange = Math.max(bottom - getHeight() + getPaddingBottom(), 0);
scrolled = Math.min(dy, scrollRange);
} else {
scrolled = dy;
}
if ((bottom - scrolled) < getHeight()) { // new row scrolled in
int nextRow = lastVisibleRow + 1;
if (nextRow < getSpannedRowCount()) {
int startOffset = top - (firstVisibleRow * cellHeight);
layoutRow(nextRow, startOffset, recycler, state);
}
}
int lastPositionInRow = getLastPositionInSpannedRow(firstVisibleRow, state);
int bottomOfFirstRow =
getDecoratedBottom(getChildAt(lastPositionInRow - firstVisiblePosition));
if (bottomOfFirstRow - scrolled < 0) { // first spanned row scrolled out
recycleRow(firstVisibleRow, recycler, state);
}
}
offsetChildrenVertical(-scrolled);
return scrolled;
}
@Override
public void scrollToPosition(int position) {
if (position >= getItemCount()) position = getItemCount() - 1;
firstVisibleRow = getRowIndex(position);
resetVisibleItemTracking();
forceClearOffsets = true;
removeAllViews();
requestLayout();
}
@Override
public void smoothScrollToPosition(
RecyclerView recyclerView, RecyclerView.State state, int position) {
if (position >= getItemCount()) position = getItemCount() - 1;
LinearSmoothScroller scroller = new LinearSmoothScroller(recyclerView.getContext()) {
@Override
public PointF computeScrollVectorForPosition(int targetPosition) {
final int rowOffset = getRowIndex(targetPosition) - firstVisibleRow;
return new PointF(0, rowOffset * cellHeight);
}
};
scroller.setTargetPosition(position);
startSmoothScroll(scroller);
}
@Override
public int computeVerticalScrollRange(RecyclerView.State state) {
// TODO update this to incrementally calculate
return getSpannedRowCount() * cellHeight + getPaddingTop() + getPaddingBottom();
}
@Override
public int computeVerticalScrollExtent(RecyclerView.State state) {
return getHeight();
}
@Override
public int computeVerticalScrollOffset(RecyclerView.State state) {
if (getChildCount() == 0) return 0;
return getPaddingTop() + (firstVisibleRow * cellHeight) - getDecoratedTop(getChildAt(0));
}
@Override
public View findViewByPosition(int position) {
if (position < firstVisiblePosition || position > lastVisiblePosition) return null;
return getChildAt(position - firstVisiblePosition);
}
public int getFirstVisibleItemPosition() {
return firstVisiblePosition;
}
private static class GridCell {
final int row;
final int rowSpan;
final int column;
final int columnSpan;
GridCell(int row, int rowSpan, int column, int columnSpan) {
this.row = row;
this.rowSpan = rowSpan;
this.column = column;
this.columnSpan = columnSpan;
}
}
/**
* This is the main layout algorithm, iterates over all items and places them into [column, row]
* cell positions. Stores this layout info for use later on. Also records the adapter position
* that each row starts at.
* <p>
* Note that if a row is spanned, then the row start position is recorded as the first cell of
* the row that the spanned cell starts in. This is to ensure that we have sufficient contiguous
* views to layout/draw a spanned row.
*/
private void calculateCellPositions(RecyclerView.Recycler recycler, RecyclerView.State state) {
final int itemCount = state.getItemCount();
cells = new SparseArray<>(itemCount);
firstChildPositionForRow = new ArrayList<>();
int row = 0;
int column = 0;
recordSpannedRowStartPosition(row, column);
int[] rowHWM = new int[columns]; // row high water mark (per column)
for (int position = 0; position < itemCount; position++) {
SpanInfo spanInfo;
int adapterPosition = recycler.convertPreLayoutPositionToPostLayout(position);
if (adapterPosition != RecyclerView.NO_POSITION) {
spanInfo = spanLookup.getSpanInfo(adapterPosition);
} else {
// item removed from adapter, retrieve its previous span info
// as we can't get from the lookup (adapter)
spanInfo = getSpanInfoFromAttachedView(position);
}
if (spanInfo.columnSpan > columns) {
spanInfo.columnSpan = columns; // or should we throw?
}
// check horizontal space at current position else start a new row
// note that this may leave gaps in the grid; we don't backtrack to try and fit
// subsequent cells into gaps. We place the responsibility on the adapter to provide
// continuous data i.e. that would not span column boundaries to avoid gaps.
if (column + spanInfo.columnSpan > columns) {
row++;
recordSpannedRowStartPosition(row, position);
column = 0;
}
// check if this cell is already filled (by previous spanning cell)
while (rowHWM[column] > row) {
column++;
if (column + spanInfo.columnSpan > columns) {
row++;
recordSpannedRowStartPosition(row, position);
column = 0;
}
}
// by this point, cell should fit at [column, row]
cells.put(position, new GridCell(row, spanInfo.rowSpan, column, spanInfo.columnSpan));
// update the high water mark book-keeping
for (int columnsSpanned = 0; columnsSpanned < spanInfo.columnSpan; columnsSpanned++) {
rowHWM[column + columnsSpanned] = row + spanInfo.rowSpan;
}
// if we're spanning rows then record the 'first child position' as the first item
// *in the row the spanned item starts*. i.e. the position might not actually sit
// within the row but it is the earliest position we need to render in order to fill
// the requested row.
if (spanInfo.rowSpan > 1) {
int rowStartPosition = getFirstPositionInSpannedRow(row);
for (int rowsSpanned = 1; rowsSpanned < spanInfo.rowSpan; rowsSpanned++) {
int spannedRow = row + rowsSpanned;
recordSpannedRowStartPosition(spannedRow, rowStartPosition);
}
}
// increment the current position
column += spanInfo.columnSpan;
}
totalRows = rowHWM[0];
for (int i = 1; i < rowHWM.length; i++) {
if (rowHWM[i] > totalRows) {
totalRows = rowHWM[i];
}
}
}
private SpanInfo getSpanInfoFromAttachedView(int position) {
for (int i = 0; i < getChildCount(); i++) {
View child = getChildAt(i);
if (position == getPosition(child)) {
LayoutParams lp = (LayoutParams) child.getLayoutParams();
return new SpanInfo(lp.columnSpan, lp.rowSpan);
}
}
// errrrr?
return SpanInfo.SINGLE_CELL;
}
private void recordSpannedRowStartPosition(final int rowIndex, final int position) {
if (getSpannedRowCount() < (rowIndex + 1)) {
firstChildPositionForRow.add(position);
}
}
private int getRowIndex(final int position) {
return position < cells.size() ? cells.get(position).row : -1;
}
private int getSpannedRowCount() {
return firstChildPositionForRow.size();
}
private int getNextSpannedRow(int rowIndex) {
int firstPositionInRow = getFirstPositionInSpannedRow(rowIndex);
int nextRow = rowIndex + 1;
while (nextRow < getSpannedRowCount()
&& getFirstPositionInSpannedRow(nextRow) == firstPositionInRow) {
nextRow++;
}
return nextRow;
}
private int getFirstPositionInSpannedRow(int rowIndex) {
return firstChildPositionForRow.get(rowIndex);
}
private int getLastPositionInSpannedRow(final int rowIndex, RecyclerView.State state) {
int nextRow = getNextSpannedRow(rowIndex);
return (nextRow != getSpannedRowCount()) ? // check if reached boundary
getFirstPositionInSpannedRow(nextRow) - 1
: state.getItemCount() - 1;
}
/**
* Lay out a given 'row'. We might actually add more that one row if the requested row contains
* a row-spanning cell. Returns the pixel height of the rows laid out.
* <p>
* To simplify logic & book-keeping, views are attached in adapter order, that is child 0 will
* always be the earliest position displayed etc.
*/
private int layoutRow(
int rowIndex, int startTop, RecyclerView.Recycler recycler, RecyclerView.State state) {
int firstPositionInRow = getFirstPositionInSpannedRow(rowIndex);
int lastPositionInRow = getLastPositionInSpannedRow(rowIndex, state);
boolean containsRemovedItems = false;
int insertPosition = (rowIndex < firstVisibleRow) ? 0 : getChildCount();
for (int position = firstPositionInRow;
position <= lastPositionInRow;
position++, insertPosition++) {
View view = recycler.getViewForPosition(position);
LayoutParams lp = (LayoutParams) view.getLayoutParams();
containsRemovedItems |= lp.isItemRemoved();
GridCell cell = cells.get(position);
addView(view, insertPosition);
// TODO use orientation helper
int wSpec = getChildMeasureSpec(
cellBorders[cell.column + cell.columnSpan] - cellBorders[cell.column],
View.MeasureSpec.EXACTLY, 0, lp.width, false);
int hSpec = getChildMeasureSpec(cell.rowSpan * cellHeight,
View.MeasureSpec.EXACTLY, 0, lp.height, true);
measureChildWithDecorationsAndMargin(view, wSpec, hSpec);
int left = cellBorders[cell.column] + lp.leftMargin;
int top = startTop + (cell.row * cellHeight) + lp.topMargin;
int right = left + getDecoratedMeasuredWidth(view);
int bottom = top + getDecoratedMeasuredHeight(view);
layoutDecorated(view, left, top, right, bottom);
lp.columnSpan = cell.columnSpan;
lp.rowSpan = cell.rowSpan;
}
if (firstPositionInRow < firstVisiblePosition) {
firstVisiblePosition = firstPositionInRow;
firstVisibleRow = getRowIndex(firstVisiblePosition);
}
if (lastPositionInRow > lastVisiblePosition) {
lastVisiblePosition = lastPositionInRow;
lastVisibleRow = getRowIndex(lastVisiblePosition);
}
if (containsRemovedItems) return 0; // don't consume space for rows with disappearing items
GridCell first = cells.get(firstPositionInRow);
GridCell last = cells.get(lastPositionInRow);
return (last.row + last.rowSpan - first.row) * cellHeight;
}
/**
* Remove and recycle all items in this 'row'. If the row includes a row-spanning cell then all
* cells in the spanned rows will be removed.
*/
private void recycleRow(
int rowIndex, RecyclerView.Recycler recycler, RecyclerView.State state) {
int firstPositionInRow = getFirstPositionInSpannedRow(rowIndex);
int lastPositionInRow = getLastPositionInSpannedRow(rowIndex, state);
int toRemove = lastPositionInRow;
while (toRemove >= firstPositionInRow) {
int index = toRemove - firstVisiblePosition;
removeAndRecycleViewAt(index, recycler);
toRemove--;
}
if (rowIndex == firstVisibleRow) {
firstVisiblePosition = lastPositionInRow + 1;
firstVisibleRow = getRowIndex(firstVisiblePosition);
}
if (rowIndex == lastVisibleRow) {
lastVisiblePosition = firstPositionInRow - 1;
lastVisibleRow = getRowIndex(lastVisiblePosition);
}
}
private void layoutDisappearingViews(
RecyclerView.Recycler recycler, RecyclerView.State state, int startTop) {
// TODO
}
private void calculateWindowSize() {
// TODO use OrientationHelper#getTotalSpace
int cellWidth =
(int) Math.floor((getWidth() - getPaddingLeft() - getPaddingRight()) / columns);
cellHeight = (int) Math.floor(cellWidth * (1f / cellAspectRatio));
calculateCellBorders();
}
private void reset() {
cells = null;
firstChildPositionForRow = null;
firstVisiblePosition = 0;
firstVisibleRow = 0;
lastVisiblePosition = 0;
lastVisibleRow = 0;
cellHeight = 0;
forceClearOffsets = false;
}
private void resetVisibleItemTracking() {
// maintain the firstVisibleRow but reset other state vars
// TODO make orientation agnostic
int minimumVisibleRow = getMinimumFirstVisibleRow();
if (firstVisibleRow > minimumVisibleRow) firstVisibleRow = minimumVisibleRow;
firstVisiblePosition = getFirstPositionInSpannedRow(firstVisibleRow);
lastVisibleRow = firstVisibleRow;
lastVisiblePosition = firstVisiblePosition;
}
private int getMinimumFirstVisibleRow() {
int maxDisplayedRows = (int) Math.ceil((float) getHeight() / cellHeight) + 1;
if (totalRows < maxDisplayedRows) return 0;
int minFirstRow = totalRows - maxDisplayedRows;
// adjust to spanned rows
return getRowIndex(getFirstPositionInSpannedRow(minFirstRow));
}
/* Adapted from GridLayoutManager */
private void calculateCellBorders() {
cellBorders = new int[columns + 1];
int totalSpace = getWidth() - getPaddingLeft() - getPaddingRight();
int consumedPixels = getPaddingLeft();
cellBorders[0] = consumedPixels;
int sizePerSpan = totalSpace / columns;
int sizePerSpanRemainder = totalSpace % columns;
int additionalSize = 0;
for (int i = 1; i <= columns; i++) {
int itemSize = sizePerSpan;
additionalSize += sizePerSpanRemainder;
if (additionalSize > 0 && (columns - additionalSize) < sizePerSpanRemainder) {
itemSize += 1;
additionalSize -= columns;
}
consumedPixels += itemSize;
cellBorders[i] = consumedPixels;
}
}
private void measureChildWithDecorationsAndMargin(View child, int widthSpec, int heightSpec) {
calculateItemDecorationsForChild(child, itemDecorationInsets);
RecyclerView.LayoutParams lp = (RecyclerView.LayoutParams) child.getLayoutParams();
widthSpec = updateSpecWithExtra(widthSpec, lp.leftMargin + itemDecorationInsets.left,
lp.rightMargin + itemDecorationInsets.right);
heightSpec = updateSpecWithExtra(heightSpec, lp.topMargin + itemDecorationInsets.top,
lp.bottomMargin + itemDecorationInsets.bottom);
child.measure(widthSpec, heightSpec);
}
private int updateSpecWithExtra(int spec, int startInset, int endInset) {
if (startInset == 0 && endInset == 0) {
return spec;
}
int mode = View.MeasureSpec.getMode(spec);
if (mode == View.MeasureSpec.AT_MOST || mode == View.MeasureSpec.EXACTLY) {
return View.MeasureSpec.makeMeasureSpec(
View.MeasureSpec.getSize(spec) - startInset - endInset, mode);
}
return spec;
}
/* Adapted from ConstraintLayout */
private void parseAspectRatio(String aspect) {
if (aspect != null) {
int colonIndex = aspect.indexOf(':');
if (colonIndex >= 0 && colonIndex < aspect.length() - 1) {
String nominator = aspect.substring(0, colonIndex);
String denominator = aspect.substring(colonIndex + 1);
if (nominator.length() > 0 && denominator.length() > 0) {
try {
float nominatorValue = Float.parseFloat(nominator);
float denominatorValue = Float.parseFloat(denominator);
if (nominatorValue > 0 && denominatorValue > 0) {
cellAspectRatio = Math.abs(nominatorValue / denominatorValue);
return;
}
} catch (NumberFormatException e) {
// Ignore
}
}
}
}
throw new IllegalArgumentException("Could not parse aspect ratio: '" + aspect + "'");
}}
add following code to attr file
<declare-styleable name="SpannableGridLayoutManager">
<attr name="android:orientation" />
<attr name="spanCount" />
<attr name="aspectRatio" format="string" />
</declare-styleable>
这篇关于如何使用 recyclerview (SpannableGridLayoutManager) 设计可跨网格视图的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!