Horizontal Scrolling RecyclerView with one scroll at a time - android

I'm trying to implement a Horizontal scrolling recycler view with only one scroll at a time.
I've used following LinearSnapHelper to centralize the view
public LinearSnapHelper getSnapHelper() {
return new LinearSnapHelper() {
#Override
public int findTargetSnapPosition(RecyclerView.LayoutManager layoutManager, int velocityX, int velocityY) {
View centerView = findSnapView(layoutManager);
if (centerView == null)
return RecyclerView.NO_POSITION;
int position = layoutManager.getPosition(centerView);
int targetPosition = -1;
if (layoutManager.canScrollHorizontally()) {
if (velocityX < 0) {
targetPosition = position - 1;
} else {
targetPosition = position + 1;
}
}
if (layoutManager.canScrollVertically()) {
if (velocityY < 0) {
targetPosition = position - 1;
} else {
targetPosition = position + 1;
}
}
final int firstItem = 0;
final int lastItem = layoutManager.getItemCount() - 1;
targetPosition = Math.min(lastItem, Math.max(targetPosition, firstItem));
return targetPosition;
}
};
}
And then I've attached my horizontal recycler view to snaphelper as follow :
getSnapHelper().attachToRecyclerView(accountSelectionRecyclerView);
This works near perfect and centralize item as expected.
But I want to restrict my recycler view to scroll only one item at a time.
To achieve that I've created a custom recycler view to control the fling of the my recycler but still it has no effect and I can able to scroll to two,three items if I swipe with full force.
public class HorizontalRecyclerView extends RecyclerView {
public HorizontalRecyclerView(Context context) {
super(context);
}
public HorizontalRecyclerView(Context context, #Nullable AttributeSet attrs) {
super(context, attrs);
}
public HorizontalRecyclerView(Context context, #Nullable AttributeSet attrs, int defStyle) {
super(context, attrs, defStyle);
}
#Override
public boolean fling(int velocityX, int velocityY) {
LinearLayoutManager linearLayoutManager = (LinearLayoutManager) getLayoutManager();
int lastVisibleView = linearLayoutManager.findLastVisibleItemPosition();
int firstVisibleView = linearLayoutManager.findFirstVisibleItemPosition();
View firstView = linearLayoutManager.findViewByPosition(firstVisibleView);
View lastView = linearLayoutManager.findViewByPosition(lastVisibleView);
int leftMargin = (DeviceDetailsSingleton.getInstance().getScreenWidth() - lastView.getWidth()) / 2;
int rightMargin = (DeviceDetailsSingleton.getInstance().getScreenWidth() - firstView.getWidth()) / 2 + firstView.getWidth();
int leftEdge = lastView.getLeft();
int rightEdge = firstView.getRight();
int scrollDistanceLeft = leftEdge - leftMargin;
int scrollDistanceRight = rightMargin - rightEdge;
if (velocityX > 0)
smoothScrollBy(scrollDistanceLeft, 0);
else
smoothScrollBy(-scrollDistanceRight, 0);
return true;
}
}
With normal swipe speed it's working perfectly.

Related

How to design spannable gridview using recyclerview (SpannableGridLayoutManager)

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>

Android LinearSnapHelper - how to increase scrolling/"snapping" speed?

I'm using a LinearSnapHelper to make items in my RecyclerView "snap" into place on the screen (my cards take up most of the screen, so I want them to snap into place and fill the screen on every swipe/fling/scroll).
I'm struggling with how to make the cards snap into place faster. I've tried creating a custom LinearLayoutManager (and editing the calculateSpeedPerPixel method in scrollToPosition or smoothScrollToPosition), as well as a custom RecyclerView (and editing the fling method). But nothing effects the speed that cards "snap" into place.
I suppose the issue is that I don't really understand how LinearSnapHelper "scrolls" the cards into position. It doesn't seem to use LinearLayoutManager's scrollToPosition or smoothScrollToPosition methods.
snapHelper = new LinearSnapHelper() {
#Override
public int findTargetSnapPosition(RecyclerView.LayoutManager layoutManager, int velocityX, int velocityY) {
View centerView = findSnapView(layoutManager);
if (centerView == null) {
return RecyclerView.NO_POSITION;
}
int position = layoutManager.getPosition(centerView);
int targetPosition = -1;
if (layoutManager.canScrollHorizontally()) {
if (velocityX < 0) {
targetPosition = position - 1;
} else {
targetPosition = position + 1;
}
}
if (layoutManager.canScrollVertically()) {
if (velocityY > 0) {
targetPosition = position + 1;
} else {
targetPosition = position - 1;
}
}
final int firstItem = 0;
final int lastItem = layoutManager.getItemCount() - 1;
targetPosition = Math.min(lastItem, Math.max(targetPosition, firstItem));
return targetPosition;
}
};
snapHelper.attachToRecyclerView(mRecyclerView);
As 郭玉龙 mentioned, SnapHelper call RecyclerView.smoothScrollBy() method. And it use default sQuinticInterpolator.
To change speed of snap you can do next:
public class SlowdownRecyclerView extends RecyclerView {
// Change pow to control speed.
// Bigger = faster. RecyclerView default is 5.
private static final int POW = 2;
private Interpolator interpolator;
public SlowdownRecyclerView(Context context) {
super(context);
createInterpolator();
}
public SlowdownRecyclerView(Context context, #Nullable AttributeSet attrs) {
super(context, attrs);
createInterpolator();
}
public SlowdownRecyclerView(Context context, #Nullable AttributeSet attrs, int defStyle) {
super(context, attrs, defStyle);
createInterpolator();
}
private void createInterpolator(){
interpolator = new Interpolator() {
#Override
public float getInterpolation(float t) {
t = Math.abs(t - 1.0f);
return (float) (1.0f - Math.pow(t, POW));
}
};
}
#Override
public void smoothScrollBy(int dx, int dy) {
super.smoothScrollBy(dx, dy, interpolator);
}
Or you can implement your own interpolator.
The speed of snapping scroll is affected by RecyclerView.smoothScrollBy().
Here's the snippet of source code.
Override this function to increase or decrease the speed of snapping scroll.
I wound up doing this by adding a ScrollListener to my RecycleView, and then creating a custom LinearLayoutManager and custom smoothScrollToPosition method.
final CustomLinearLayoutManager mLayoutManager = new CustomLinearLayoutManager(getActivity());
mRecyclerView.setLayoutManager(mLayoutManager);
mRecyclerView.addOnScrollListener(new RecyclerView.OnScrollListener() {
private boolean scrollingUp;
#Override
public void onScrolled(RecyclerView recyclerView, int dx, int dy) {
scrollingUp = dy < 0;
}
#Override
public void onScrollStateChanged(RecyclerView recyclerView, int newState) {
int visiblePosition = scrollingUp ? mLayoutManager.findFirstVisibleItemPosition() : mLayoutManager.findLastVisibleItemPosition();
int completelyVisiblePosition = scrollingUp ? mLayoutManager
.findFirstCompletelyVisibleItemPosition() : mLayoutManager
.findLastCompletelyVisibleItemPosition();
if (visiblePosition != completelyVisiblePosition) {
recyclerView.smoothScrollToPosition(visiblePosition);
return;
}
}
});
I achieved this using a library https://github.com/rubensousa/GravitySnapHelper
you can also override findTargetSnapPosition to get pager like scroll
tweek the scrollMsPerInch to increase / decrease speed
val snapHelper : GravitySnapHelper = GravitySnapHelper(Gravity.CENTER)
// the lower the higher the speed, default is 100f
snapHelper.scrollMsPerInch = 40f
snapHelper.attachToRecyclerView(binding?.mRecyclerView)
Actually you can modify the LinearSnapHelper and SnapHelperClass by simply copy/paste the existing code the only thing you will do is to set MILLISECONDS_PER_INCH on SnapHelper as you want and then use simply use the LinearSnapHelper you created

How to center the Clicked position in the Recyclerview

I want to center the clicked position in the Recyclerview. I am able to scroll the Recyclerview to certain position but i want to middle that position in the screen.
I used this method to scroll to that position.
videoRecyclerview.scrollToPosition(position);
If you are using a RecyclerView and LinearLayoutManager this will work:
private void scrollToCenter(View v) {
int itemToScroll = mRecyclerView.getChildPosition(v);
int centerOfScreen = mRecyclerView.getWidth() / 2 - v.getWidth() / 2;
mLayoutManager.scrollToPositionWithOffset(itemToScroll, centerOfScreen);
}
if you use linearlayoutManager, you can use this code,
linearLayoutManager.scrollToPositionWithOffset(2, 20);
(linearLayoutManager.void scrollToPositionWithOffset (int position,
int offset))
Setting the offset to 0 should align with the top
first move scroll to your item, but whenever recyclerView scrolls it just brings the item in visible region, it is never sure that the item is in center or not, so we find the center item and then check if we are on next to center to item or behind it, here is working logic
recyclerView.smoothScrollToPosition(index);
int firstVisibleItemPosition = rvLayoutManager.findFirstVisibleItemPosition();
int lastVisibleItemPosition = rvLayoutManager.findLastVisibleItemPosition();
int centerPosition = (firstVisibleItemPosition + lastVisibleItemPosition) / 2;
if (index > centerPosition) {
recyclerView.smoothScrollToPosition(index + 1);
} else if (index < centerPosition) {
recyclerView.smoothScrollToPosition(index - 1);
}
If you need smooth scroll to centre for LieanerLayoutManager both horizontal & vertical
Copy the entire code and simply call** scrollToCenter:
public void scrollToCenter(LinearLayoutManager layoutManager, RecyclerView recyclerList, int clickPosition) {
RecyclerView.SmoothScroller smoothScroller = createSnapScroller(recyclerList, layoutManager);
if (smoothScroller != null) {
smoothScroller.setTargetPosition(clickPosition);
layoutManager.startSmoothScroll(smoothScroller);
}
}
// This number controls the speed of smooth scroll
private static final float MILLISECONDS_PER_INCH = 70f;
private final static int DIMENSION = 2;
private final static int HORIZONTAL = 0;
private final static int VERTICAL = 1;
#Nullable
private LinearSmoothScroller createSnapScroller(RecyclerView mRecyclerView, RecyclerView.LayoutManager layoutManager) {
if (!(layoutManager instanceof RecyclerView.SmoothScroller.ScrollVectorProvider)) {
return null;
}
return new LinearSmoothScroller(mRecyclerView.getContext()) {
#Override
protected void onTargetFound(View targetView, RecyclerView.State state, Action action) {
int[] snapDistances = calculateDistanceToFinalSnap(layoutManager, targetView);
final int dx = snapDistances[HORIZONTAL];
final int dy = snapDistances[VERTICAL];
final int time = calculateTimeForDeceleration(Math.max(Math.abs(dx), Math.abs(dy)));
if (time > 0) {
action.update(dx, dy, time, mDecelerateInterpolator);
}
}
#Override
protected float calculateSpeedPerPixel(DisplayMetrics displayMetrics) {
return MILLISECONDS_PER_INCH / displayMetrics.densityDpi;
}
};
}
private int[] calculateDistanceToFinalSnap(#NonNull RecyclerView.LayoutManager layoutManager, #NonNull View targetView) {
int[] out = new int[DIMENSION];
if (layoutManager.canScrollHorizontally()) {
out[HORIZONTAL] = distanceToCenter(layoutManager, targetView,
OrientationHelper.createHorizontalHelper(layoutManager));
}
if (layoutManager.canScrollVertically()) {
out[VERTICAL] = distanceToCenter(layoutManager, targetView,
OrientationHelper.createHorizontalHelper(layoutManager));
}
return out;
}
private int distanceToCenter(#NonNull RecyclerView.LayoutManager layoutManager,
#NonNull View targetView, OrientationHelper helper) {
final int childCenter = helper.getDecoratedStart(targetView)
+ (helper.getDecoratedMeasurement(targetView) / 2);
final int containerCenter;
if (layoutManager.getClipToPadding()) {
containerCenter = helper.getStartAfterPadding() + helper.getTotalSpace() / 2;
} else {
containerCenter = helper.getEnd() / 2;
}
return childCenter - containerCenter;
}
it's worked for me with this code :
layoutManager.scrollToPositionWithOffset(pos - 1,0);
//on the click callback
view.OnClickListener { callback?.onItemClicked(it)}
// code in activity or your fragment
override fun onItemClicked(view: View) {
val position = recyclerView.getChildLayoutPosition(view)
recyclerView.smoothScrollToPosition(position)
}
simplest hack ever, this was good enough for me:
videoRecyclerview.scrollToPosition(position+2);
if position+2 is within the arraylist.

Android RecyclerView with GridLayoutManager make item span multiple rows

I have a collection of photos, and I'm using a RecyclerView to display them. I want to have the first element in my RecyclerView span 2 columns AND 2 rows:
I know I can span 2 columns with setSpanSizeLookup:
GridLayoutManager gridLayoutManager = new GridLayoutManager(this, 3);
gridLayoutManager.setSpanSizeLookup(new GridLayoutManager.SpanSizeLookup() {
#Override
public int getSpanSize(int position) {
if (position == 0) {
return 2;
} else {
return 1;
}
}
});
but how can I also make the first item span 2 rows as well?
I have tried setting the first item's height to be different by inflating a different layout with double the height of the others, but that resulted in every item on the same row as the first item also being stretched to that height:
#Override
public ProfilePicViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
View itemView;
if (viewType == TYPE_MAIN_PHOTO) {
itemView = LayoutInflater.from(parent.getContext()).inflate(R.layout.card_view_main_profile_photo, parent, false);
} else {
itemView = LayoutInflater.from(parent.getContext()).inflate(R.layout.card_view_profile_photo, parent, false);
}
return new ProfilePicViewHolder(itemView);
}
You cannot achieve this behavior with GridLayoutManager, because it only supports spanning multiple columns.
Nick Butcher is currently implementing a custom SpannedGridLayoutManager that does exactly what you want. It allows you to span multiple rows and columns at the same time. The implementation is still WIP, but already works quite well.
SpannedGridLayoutManager.java
package io.plaidapp.ui.recyclerview;
import android.content.Context;
import android.content.res.TypedArray;
import android.graphics.PointF;
import android.graphics.Rect;
import android.support.annotation.Keep;
import android.support.annotation.NonNull;
import android.support.v7.widget.LinearSmoothScroller;
import android.support.v7.widget.RecyclerView;
import android.util.AttributeSet;
import android.util.SparseArray;
import android.view.View;
import android.view.ViewGroup;
import java.util.ArrayList;
import java.util.List;
import io.plaidapp.R;
/**
* A {#link RecyclerView.LayoutManager} which displays a regular grid (i.e. all cells are the same
* size) and allows simultaneous row & column spanning.
*/
public class SpannedGridLayoutManager 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 SpannedGridLayoutManager(GridSpanLookup spanLookup, int columns, float cellAspectRatio) {
this.spanLookup = spanLookup;
this.columns = columns;
this.cellAspectRatio = cellAspectRatio;
setAutoMeasureEnabled(true);
}
#Keep /* XML constructor, see RecyclerView#createLayoutManager */
public SpannedGridLayoutManager(
Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
TypedArray a = context.obtainStyledAttributes(
attrs, R.styleable.SpannedGridLayoutManager, defStyleAttr, defStyleRes);
columns = a.getInt(R.styleable.SpannedGridLayoutManager_spanCount, 1);
parseAspectRatio(a.getString(R.styleable.SpannedGridLayoutManager_aspectRatio));
// TODO use this!
int orientation = a.getInt(
R.styleable.SpannedGridLayoutManager_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
if (firstChildPositionForRow == null) return 0;
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 + "'");
}
}
attrs.xml
<?xml version="1.0" encoding="utf-8"?>
<resources>
<declare-styleable name="SpannedGridLayoutManager">
<attr name="android:orientation" />
<attr name="spanCount" />
<attr name="aspectRatio" format="string" />
</declare-styleable>
</resources>
The code is also available here.
Example usage
The code requires RecyclerView 23.2.0 or higher.
So add the following line to your build.gradle, if you didn't already do so.
dependencies {
compile 'com.android.support:recyclerview-v7:24.2.1'
}
To achieve the layout shown in the initial post, we define the LayoutManager as follows
recyclerView.setLayoutManager(new SpannedGridLayoutManager(
new SpannedGridLayoutManager.GridSpanLookup() {
#Override
public SpannedGridLayoutManager.SpanInfo getSpanInfo(int position) {
if (position == 0) {
return new SpannedGridLayoutManager.SpanInfo(2, 2);
} else {
return new SpannedGridLayoutManager.SpanInfo(1, 1);
}
}
},
3 /* Three columns */,
1f /* We want our items to be 1:1 ratio */));
You can use SpannedGridLayoutManager library wrote by Arasthel in here
This is the result
You can achieve this behavior by using RecycleView for rows only, with ViewHolder for each row. So you will have RowViewHolder for simple rows and something like DoubleRowViewHolder for custom layout that will have 3 items, just the way you want.

How to create scrollable page of carousels in Android?

I am attempting to build a UI for my Android app which contains a vertically scrollable page of horizontally scrollable carousels (something like what the Netflix app does). How is this type of behaviour accomplished?
A basic implementation would be enough to get me started. There are a few other requirements for the UI, which I'll include here for reference, since it may impact what classes or libraries I can use.
1) Vertical scrolling between carousels should be smooth, but when user releases, the UI should "snap to" the closest carousel (so the user is always on a carousel row, not between two carousels).
2) Horizontal scrolling on a carousel should be smooth, but when user releases, the UI should "snap to" the closest item in the carousel.
3) Should be possible to overlay additional information over an item in the carousel
4) UI should be adaptable to any screen size.
5) Should be navigable with the arrow keys (for touchscreen-less devices)
6) Should work on a wide range of Android versions (possibly through the support library)
7) Should be OK to use in an open-source app licensed under the GPL
Acceptable answers DO NOT have to meet all of these requirements. At a minimum, a good answer should involve navigating multiple carousels (versus only one carousel).
Here is a mock-up of basically what I am envisioning (I'm flexible, doesn't have to look like this.. point is just to clarify what I am talking about -- each row would contain a lot of items that could be scrolled left and right, and the whole page could be scrolled up and down)
Main Idea
In order to have a flexible design and having unlimited items you can create a RecyclerView as a root view with a LinearLayoutManager.VERTICAL as a LayoutManager. for each row you can put another RecyclerView but now with a LinearLayoutManager.HORIZONTAL as a LayoutManager.
Result
Source
Code
Requirements
1) Vertical scrolling between carousels should be smooth, but when
user releases, the UI should "snap to" the closest carousel (so the
user is always on a carousel row, not between two carousels).
2) Horizontal scrolling on a carousel should be smooth, but when user
releases, the UI should "snap to" the closest item in the carousel.
In order to achieve those I used OnScrollListener and when the states goes SCROLL_STATE_IDLE I check top and bottom views to see which of them has more visible region then scroll to that position. for each rows I do so for left and right views for each row adapter. In this way always one side of your carousels or rows fit. for example if top is fitted the bottom is not or vise versa. I think if you play a little more you can achieve that but you must know the dimension of window and change the dimension of carousels at runtime.
3) Should be possible to overlay additional information over an item
in the carousel
If you use RelativeLayout or FrameLayout as a root view of each item you can put information on top of each other. as you can see the numbers are on the top of images.
4) UI should be adaptable to any screen size.
if you know how to support multiple screen size you can do so easily, if you do not know read the document.
Supporting Multiple Screens
5) Should be navigable with the arrow keys (for touchscreen-less
devices)
use below function
mRecyclerView.scrollToPosition(position);
6) Should work on a wide range of Android versions (possibly through
the support library)
import android.support.v7.widget.RecyclerView;
7) Should be OK to use in an open-source app licensed under the GPL
Ok
happy coding!!
You can use ListView with a custom OnTouchListener (for snapping items) for the vertical scrolling and TwoWayGridView again with a custom OnTouchListener (for snapping items)
main.xml
<ListView xmlns:android="http://schemas.android.com/apk/res/android"
android:id="#+id/containerList"
android:layout_width="match_parent"
android:layout_height="300dp"
android:background="#E8E8E8"
android:divider="#android:color/transparent"
android:dividerHeight="16dp" />
list_item_hgrid.xml
<com.jess.ui.TwoWayGridView xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:id="#+id/grid"
android:layout_width="match_parent"
android:layout_height="160dp"
android:layout_marginBottom="16dp"
app:cacheColorHint="#E8E8E8"
app:columnWidth="128dp"
app:gravity="center"
app:horizontalSpacing="16dp"
app:numColumns="auto_fit"
app:numRows="1"
app:rowHeight="128dp"
app:scrollDirectionLandscape="horizontal"
app:scrollDirectionPortrait="horizontal"
app:stretchMode="spacingWidthUniform"
app:verticalSpacing="16dp" />
And the Activity code will be something like the following
#Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.test);
ListView containerList = (ListView) findViewById(R.id.containerList);
containerList.setAdapter(new DummyGridsAdapter(this));
containerList.setOnTouchListener(mContainerListOnTouchListener);
}
private View.OnTouchListener mContainerListOnTouchListener = new View.OnTouchListener() {
#Override
public boolean onTouch(View view, MotionEvent event) {
switch (event.getAction()) {
case MotionEvent.ACTION_UP:
View itemView = ((ListView) view).getChildAt(0);
int top = itemView.getTop();
if (Math.abs(top) >= itemView.getHeight() / 2) {
top = itemView.getHeight() - Math.abs(top);
}
((ListView) view).smoothScrollBy(top, 400);
}
return false;
}
};
And here are the test adapters
private static class DummyGridsAdapter extends BaseAdapter {
private Context mContext;
private TwoWayGridView[] mChildGrid;
public DummyGridsAdapter(Context context) {
mContext = context;
mChildGrid = new TwoWayGridView[getCount()];
for (int i = 0; i < mChildGrid.length; i++) {
mChildGrid[i] = (TwoWayGridView) LayoutInflater.from(context).
inflate(R.layout.list_item_hgrid, null);
mChildGrid[i].setAdapter(new DummyImageAdapter(context));
mChildGrid[i].setOnTouchListener(mChildGridOnTouchListener);
}
}
#Override
public int getCount() {
return 8;
}
#Override
public Object getItem(int position) {
return position;
}
#Override
public long getItemId(int position) {
return 0;
}
#Override
public View getView(int position, View convertView, ViewGroup parent) {
return mChildGrid[position];
}
private View.OnTouchListener mChildGridOnTouchListener = new View.OnTouchListener() {
#Override
public boolean onTouch(View view, MotionEvent event) {
switch (event.getAction()) {
case MotionEvent.ACTION_UP:
View itemView = ((TwoWayGridView) view).getChildAt(0);
int left = itemView.getLeft();
if (Math.abs(left) >= itemView.getWidth() / 2) {
left = itemView.getWidth() - Math.abs(left);
}
((TwoWayGridView) view).smoothScrollBy(left, 400);
}
return false;
}
};
}
private static class DummyImageAdapter extends BaseAdapter {
private Context mContext;
private final int mDummyViewWidthHeight;
public DummyImageAdapter(Context context) {
mContext = context;
mDummyViewWidthHeight = (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 128,
context.getResources().getDisplayMetrics());
}
#Override
public int getCount() {
return 16;
}
#Override
public Object getItem(int position) {
int component = (getCount() - position - 1) * 255 / getCount();
return Color.argb(255, 255, component, component);
}
#Override
public long getItemId(int position) {
return 0;
}
#Override
public View getView(int position, View convertView, ViewGroup parent) {
ImageView imageView = new ImageView(mContext);
imageView.setBackgroundColor((Integer) getItem(position));
imageView.setLayoutParams(new TwoWayGridView.LayoutParams(mDummyViewWidthHeight, mDummyViewWidthHeight));
return imageView;
}
}
I would suggest the Recycler view.
You can create horizontal and vertical list or gridviews. In my opinion the viewpager can become complicated at times.
I'm working on video on demand application and this saved me.
In your case it will be easy to set up. I will give you some code.
You will need the following:
XML View - Where the recycle layout is declared.
Adapter - You will need a view to populate the adapter and fill the recycleview.
Creating the view
<android.support.v7.widget.RecyclerView
android:id="#+id/recycle_view"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:scrollbars="none"
android:orientation="horizontal"
android:gravity="center"
android:overScrollMode="never"/>
Declare this where you want the carousel to display.
Next you want to create the adapter:
public class HorizontalCarouselItemAdapter extends RecyclerView.Adapter<HorizontalCarouselItemAdapter.ViewHolder> {
List<objects> items;
int itemLayout;
public HorizontalCarouselItemAdapter(Context context, int itemLayout, List<objects> items) {
this.context = context;
this.itemLayout = itemLayout;
this.items = items;
}
#Override public ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
View v = LayoutInflater.from(parent.getContext()).inflate(itemLayout, parent, false);
return new ViewHolder(v);
}
#Override public void onBindViewHolder(final ViewHolder holder, final int position) {
this.holders = holder;
final GenericAsset itemAdapter = items.get(position);
holder.itemImage.setDrawable //manipulate variables here
}
#Override public int getItemCount() {
return items.size();
}
public static class ViewHolder extends RecyclerView.ViewHolder {
public ImageView itemImage;
public ViewHolder(View itemView) {
super(itemView);
itemImage = (ImageView) itemView.findViewById(R.id.carousel_cell_holder_image);
}
}
This is where you feed the data to the adapter to populate each carousel item.
Finally declare it and call the adapter:
recyclerView = (RecyclerView)findViewById(R.id.recycle_view);
ListLayoutManager manager = new ListLayoutManager(getApplication(), ListLayoutManager.Orientation.HORIZONTAL);
recyclerView.setLayoutManager(manager);
CustomAdpater adapter = new CustomAdapter(getApplication(), data);
recyclerView.setAdapter(adapter);
You can create a listview with recycle views to achieve what you want.
This class is great for smooth scrolling and memory optimisation.
This is the link for it:
https://developer.android.com/reference/android/support/v7/widget/RecyclerView.html
I hope this helps you.
You can use a ScrollView as parent inside that ScrollView place a Vertical LinearLayout in for loop inflate a layout which consist coverflow for carousel effect
github link of android-coverflow
I needed somthing like that a while back, I just used that : https://github.com/simonrob/Android-Horizontal-ListView
Simple, powerful, customizable.
Example of my version :
public class HorizontalListView extends AdapterView<ListAdapter> {
public boolean mAlwaysOverrideTouch = true;
protected ListAdapter mAdapter;
private int mLeftViewIndex = -1;
private int mRightViewIndex = 0;
protected int mCurrentX;
protected int mNextX;
private int mMaxX = Integer.MAX_VALUE;
private int mDisplayOffset = 0;
protected Scroller mScroller;
private GestureDetector mGesture;
private Queue<View> mRemovedViewQueue = new LinkedList<View>();
private OnItemSelectedListener mOnItemSelected;
private OnItemClickListener mOnItemClicked;
private OnItemLongClickListener mOnItemLongClicked;
private boolean mDataChanged = false;
public HorizontalListView(Context context, AttributeSet attrs) {
super(context, attrs);
initView();
}
private synchronized void initView() {
mLeftViewIndex = -1;
mRightViewIndex = 0;
mDisplayOffset = 0;
mCurrentX = 0;
mNextX = 0;
mMaxX = Integer.MAX_VALUE;
mScroller = new Scroller(getContext());
mGesture = new GestureDetector(getContext(), mOnGesture);
}
#Override
public void setOnItemSelectedListener(AdapterView.OnItemSelectedListener listener) {
mOnItemSelected = listener;
}
#Override
public void setOnItemClickListener(AdapterView.OnItemClickListener listener) {
mOnItemClicked = listener;
}
#Override
public void setOnItemLongClickListener(AdapterView.OnItemLongClickListener listener) {
mOnItemLongClicked = listener;
}
private DataSetObserver mDataObserver = new DataSetObserver() {
#Override
public void onChanged() {
synchronized (HorizontalListView.this) {
mDataChanged = true;
}
invalidate();
requestLayout();
}
#Override
public void onInvalidated() {
reset();
invalidate();
requestLayout();
}
};
#Override
public ListAdapter getAdapter() {
return mAdapter;
}
#Override
public View getSelectedView() {
//TODO: implement
return null;
}
#Override
public void setAdapter(ListAdapter adapter) {
if (mAdapter != null) {
mAdapter.unregisterDataSetObserver(mDataObserver);
}
mAdapter = adapter;
mAdapter.registerDataSetObserver(mDataObserver);
reset();
}
private synchronized void reset() {
initView();
removeAllViewsInLayout();
requestLayout();
}
#Override
public void setSelection(int position) {
//TODO: implement
}
private void addAndMeasureChild(final View child, int viewPos) {
LayoutParams params = child.getLayoutParams();
if (params == null) {
params = new LayoutParams(LayoutParams.FILL_PARENT, LayoutParams.FILL_PARENT);
}
addViewInLayout(child, viewPos, params, true);
child.measure(MeasureSpec.makeMeasureSpec(getWidth(), MeasureSpec.AT_MOST),
MeasureSpec.makeMeasureSpec(getHeight(), MeasureSpec.AT_MOST));
}
#Override
protected synchronized void onLayout(boolean changed, int left, int top, int right, int bottom) {
super.onLayout(changed, left, top, right, bottom);
if (mAdapter == null) {
return;
}
if (mDataChanged) {
int oldCurrentX = mCurrentX;
initView();
removeAllViewsInLayout();
mNextX = oldCurrentX;
mDataChanged = false;
}
if (mScroller.computeScrollOffset()) {
mNextX = mScroller.getCurrX();
}
if (mNextX <= 0) {
mNextX = 0;
mScroller.forceFinished(true);
}
if (mNextX >= mMaxX) {
mNextX = mMaxX;
mScroller.forceFinished(true);
}
int dx = mCurrentX - mNextX;
removeNonVisibleItems(dx);
fillList(dx);
positionItems(dx);
mCurrentX = mNextX;
if (!mScroller.isFinished()) {
post(new Runnable() {
#Override
public void run() {
requestLayout();
}
});
}
}
private void fillList(final int dx) {
int edge = 0;
View child = getChildAt(getChildCount() - 1);
if (child != null) {
edge = child.getRight();
}
fillListRight(edge, dx);
edge = 0;
child = getChildAt(0);
if (child != null) {
edge = child.getLeft();
}
fillListLeft(edge, dx);
}
private void fillListRight(int rightEdge, final int dx) {
while (rightEdge + dx < getWidth() && mRightViewIndex < mAdapter.getCount()) {
View child = mAdapter.getView(mRightViewIndex, mRemovedViewQueue.poll(), this);
addAndMeasureChild(child, -1);
rightEdge += child.getMeasuredWidth();
if (mRightViewIndex == mAdapter.getCount() - 1) {
mMaxX = mCurrentX + rightEdge - getWidth();
}
if (mMaxX < 0) {
mMaxX = 0;
}
mRightViewIndex++;
}
}
private void fillListLeft(int leftEdge, final int dx) {
while (leftEdge + dx > 0 && mLeftViewIndex >= 0) {
View child = mAdapter.getView(mLeftViewIndex, mRemovedViewQueue.poll(), this);
addAndMeasureChild(child, 0);
leftEdge -= child.getMeasuredWidth();
mLeftViewIndex--;
mDisplayOffset -= child.getMeasuredWidth();
}
}
private void removeNonVisibleItems(final int dx) {
View child = getChildAt(0);
while (child != null && child.getRight() + dx <= 0) {
mDisplayOffset += child.getMeasuredWidth();
mRemovedViewQueue.offer(child);
removeViewInLayout(child);
mLeftViewIndex++;
child = getChildAt(0);
}
child = getChildAt(getChildCount() - 1);
while (child != null && child.getLeft() + dx >= getWidth()) {
mRemovedViewQueue.offer(child);
removeViewInLayout(child);
mRightViewIndex--;
child = getChildAt(getChildCount() - 1);
}
}
private void positionItems(final int dx) {
if (getChildCount() > 0) {
mDisplayOffset += dx;
int left = mDisplayOffset;
for (int i = 0; i < getChildCount(); i++) {
View child = getChildAt(i);
int childWidth = child.getMeasuredWidth();
child.layout(left, 0, left + childWidth, child.getMeasuredHeight());
left += childWidth;
}
}
}
public synchronized void scrollTo(int x) {
mScroller.startScroll(mNextX, 0, x - mNextX, 0);
requestLayout();
}
public synchronized void scrollToChild(int position) {
//TODO
requestLayout();
}
#Override
public boolean dispatchTouchEvent(MotionEvent ev) {
return mGesture.onTouchEvent(ev);
}
protected boolean onFling(MotionEvent e1, MotionEvent e2, float velocityX,
float velocityY) {
synchronized (HorizontalListView.this) {
mScroller.fling(mNextX, 0, (int) -velocityX, 0, 0, mMaxX, 0, 0);
}
requestLayout();
return true;
}
protected boolean onDown(MotionEvent e) {
mScroller.forceFinished(true);
return true;
}
private OnGestureListener mOnGesture = new GestureDetector.SimpleOnGestureListener() {
#Override
public boolean onDown(MotionEvent e) {
return HorizontalListView.this.onDown(e);
}
#Override
public boolean onFling(MotionEvent e1, MotionEvent e2, float velocityX,
float velocityY) {
return HorizontalListView.this.onFling(e1, e2, velocityX, velocityY);
}
#Override
public boolean onScroll(MotionEvent e1, MotionEvent e2,
float distanceX, float distanceY) {
synchronized (HorizontalListView.this) {
mNextX += (int) distanceX;
}
requestLayout();
return true;
}
#Override
public boolean onSingleTapConfirmed(MotionEvent e) {
Rect viewRect = new Rect();
for (int i = 0; i < getChildCount(); i++) {
View child = getChildAt(i);
int left = child.getLeft();
int right = child.getRight();
int top = child.getTop();
int bottom = child.getBottom();
viewRect.set(left, top, right, bottom);
if (viewRect.contains((int) e.getX(), (int) e.getY())) {
if (mOnItemClicked != null) {
mOnItemClicked.onItemClick(HorizontalListView.this, child, mLeftViewIndex + 1 + i, mAdapter.getItemId(mLeftViewIndex + 1 + i));
}
if (mOnItemSelected != null) {
mOnItemSelected.onItemSelected(HorizontalListView.this, child, mLeftViewIndex + 1 + i, mAdapter.getItemId(mLeftViewIndex + 1 + i));
}
break;
}
}
return true;
}
#Override
public void onLongPress(MotionEvent e) {
Rect viewRect = new Rect();
int childCount = getChildCount();
for (int i = 0; i < childCount; i++) {
View child = getChildAt(i);
int left = child.getLeft();
int right = child.getRight();
int top = child.getTop();
int bottom = child.getBottom();
viewRect.set(left, top, right, bottom);
if (viewRect.contains((int) e.getX(), (int) e.getY())) {
if (mOnItemLongClicked != null) {
mOnItemLongClicked.onItemLongClick(HorizontalListView.this, child, mLeftViewIndex + 1 + i, mAdapter.getItemId(mLeftViewIndex + 1 + i));
}
break;
}
}
}
};
}
Here is the XML :
<com.example.package.widgets.HorizontalListView
android:id="#+id/horizontal_listview"
android:layout_marginTop="30dp"
android:layout_marginLeft="10dp"
android:layout_marginRight="10dp"
android:layout_width="fill_parent"
android:layout_height="80dp"
android:background="#color/light_gray"
/>
In the OnCreate :
mAdapter = new ArrayAdapter<Uri>(this, R.layout.viewitem) {
#Override
public int getCount() {
return listUriAdapter.size();
}
#Override
public Uri getItem(int position) {
return listUriAdapter.get(position);
}
#Override
public long getItemId(int position) {
return 0;
}
#Override
public View getView(final int position, View convertView, ViewGroup parent) {
// do what you have to do
return retval;
}
};
onItemClickListener = new AdapterView.OnItemClickListener() {
#Override
public void onItemClick(AdapterView<?> adapterView, View view, int i, long l) {
}
};
onItemLongClickListener = new AdapterView.OnItemLongClickListener() {
#Override
public boolean onItemLongClick(AdapterView<?> adapterView, View view, int i, long l) {
return false;
}
};
horizontalListView.setOnItemClickListener(onItemClickListener);
horizontalListView.setOnItemLongClickListener(onItemLongClickListener);
horizontalListView.setAdapter(mAdapter);

Categories

Resources