I have a GridView
that has items inside 5x50.
I need to scroll them in all directions and instead of stopping when reached the end just start from the top/left.
for example left-to-right scroll
before scroll
1 2 3 4 5
6 7 8 9 10
after scroll to the right
5 1 2 3 4
10 6 7 8 9
and for top-to-bottom (or bottom-to-top)
before scroll to the bottom
1 2 3 4 5
6 7 8 9 10
11 12 13 14 15
after scroll
6 7 8 9 10
11 12 13 14 15
1 2 3 4 5
I try to make it smooth scroll as GridView
native scroll.
Here is another solution but with a canvas approach.
activity_main.xml
<?xml version="1.0" encoding="utf-8"?>
<android.support.constraint.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/constraint_layout"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".MainActivity">
<nice.fontaine.infinitescroll.CanvasView
android:id="@+id/canvas_view"
android:layout_width="match_parent"
android:layout_height="match_parent"/>
</android.support.constraint.ConstraintLayout>
MainActivity.java
public class MainActivity extends AppCompatActivity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
CanvasView canvas = findViewById(R.id.canvas_view);
String[][] labels = new String[][] {
{"5", "8", "2"},
{"4", "7", "1"},
{"3", "6", "9"}
};
int columns = 3;
int rows = 3;
canvas.with(labels, columns, rows);
}
}
CanvasView.java
public class CanvasView extends View {
private final Panning panning;
private final GridManager gridManager;
private Rect bounds;
private Point current = new Point(0, 0);
private List<Overlay> overlays;
public CanvasView(Context context, AttributeSet attrs) {
super(context, attrs);
bounds = new Rect();
panning = new Panning();
overlays = new ArrayList<>();
gridManager = new GridManager(this);
init();
}
public void with(String[][] labels, int columns, int rows) {
gridManager.with(labels, columns, rows);
}
private void init() {
ViewTreeObserver observer = getViewTreeObserver();
observer.addOnGlobalLayoutListener(new ViewTreeObserver.OnGlobalLayoutListener() {
@Override
public void onGlobalLayout() {
int width = getWidth();
int height = getHeight();
bounds.set(0, 0, width, height);
gridManager.generate(bounds);
getViewTreeObserver().removeOnGlobalLayoutListener(this);
}
});
}
@Override
protected void onSizeChanged(int width, int height, int oldWidth, int oldHeight) {
super.onSizeChanged(width, height, oldWidth, oldHeight);
Bitmap bitmap = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888);
new Canvas(bitmap);
}
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
bounds.offsetTo(-current.x, -current.y);
gridManager.generate(bounds);
canvas.translate(current.x, current.y);
for (Overlay overlay : overlays) {
if (overlay.intersects(bounds)) {
overlay.onDraw(canvas);
}
}
}
@Override
public boolean onTouchEvent(MotionEvent event) {
current = panning.handle(event);
invalidate();
return true;
}
public void addChild(Overlay overlay) {
this.overlays.add(overlay);
}
}
GridManager.java
class GridManager {
private final CanvasView canvas;
private int columns;
private int rows;
private String[][] labels;
private final Map<String, Overlay> cache;
GridManager(CanvasView canvas) {
this.canvas = canvas;
cache = new HashMap<>();
}
void with(String[][] labels, int columns, int rows) {
this.columns = columns;
this.rows = rows;
this.labels = labels;
}
void generate(Rect bounds) {
if (columns == 0 || rows == 0 || labels == null) return;
int width = bounds.width();
int height = bounds.height();
int overlayWidth = width / columns;
int overlayHeight = height / rows;
int minX = mod(floor(bounds.left, overlayWidth), columns);
int minY = mod(floor(bounds.top, overlayHeight), rows);
int startX = floorToMod(bounds.left, overlayWidth);
int startY = floorToMod(bounds.top, overlayHeight);
for (int j = 0; j <= rows; j++) {
for (int i = 0; i <= columns; i++) {
String label = getLabel(minX, minY, i, j);
int x = startX + i * overlayWidth;
int y = startY + j * overlayHeight;
String key = x + "_" + y;
if (!cache.containsKey(key)) {
Overlay overlay = new Overlay(label, x, y, overlayWidth, overlayHeight);
cache.put(key, overlay);
canvas.addChild(overlay);
}
}
}
}
private String getLabel(int minX, int minY, int i, int j) {
int m = mod(minX + i, columns);
int n = mod(minY + j, rows);
return labels[n][m];
}
private int floor(double numerator, double denominator) {
return (int) Math.floor(numerator / denominator);
}
private int floorToMod(int value, int modulo) {
return value - mod(value, modulo);
}
private int mod(int value, int modulo) {
return (value % modulo + modulo) % modulo;
}
}
Panning.java
class Panning {
private Point start;
private Point delta = new Point(0, 0);
private Point cursor = new Point(0, 0);
private boolean isFirst;
Point handle(MotionEvent event) {
final Point point = new Point((int) event.getX(), (int) event.getY());
switch (event.getAction()) {
case MotionEvent.ACTION_DOWN:
press();
break;
case MotionEvent.ACTION_MOVE:
drag(point);
break;
}
return new Point(cursor.x + delta.x, cursor.y + delta.y);
}
private void press() {
isFirst = true;
}
private void drag(final Point point) {
if (isFirst) {
start = point;
cursor.offset(delta.x, delta.y);
isFirst = false;
}
delta.x = point.x - start.x;
delta.y = point.y - start.y;
}
}
Overlay.java
class Overlay {
private final String text;
private final int x;
private final int y;
private final Paint paint;
private final Rect bounds;
private final Rect rect;
private final Rect textRect;
Overlay(String text, int x, int y, int width, int height) {
this.text = text;
this.bounds = new Rect(x, y, x + width, y + height);
this.rect = new Rect();
this.textRect = new Rect();
paint = new Paint();
paint.setColor(Color.BLACK);
setTextSize(text);
this.x = x + width / 2 - textRect.width() / 2;
this.y = y + height / 2 + textRect.height() / 2;
}
boolean intersects(Rect r) {
rect.set(bounds.left, bounds.top, bounds.right, bounds.bottom);
return rect.intersect(r.left, r.top, r.right, r.bottom);
}
void onDraw(Canvas canvas) {
// rectangle
paint.setStrokeWidth(5);
paint.setStyle(Paint.Style.STROKE);
canvas.drawRect(bounds, paint);
// centered text
paint.setStrokeWidth(2);
paint.setStyle(Paint.Style.FILL);
canvas.drawText(text, x, y, paint);
}
private void setTextSize(String text) {
final float testTextSize = 100f;
paint.setTextSize(testTextSize);
paint.getTextBounds(text, 0, text.length(), textRect);
}
}
Having specified following view hierarchy in activity_main.xml:
<?xml version="1.0" encoding="utf-8"?>
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent">
<android.support.v7.widget.RecyclerView
android:id="@+id/recycler"
android:layout_width="match_parent"
android:layout_height="match_parent" />
</FrameLayout>
And a basic list item - a TextView
, inside item.xml
:
<?xml version="1.0" encoding="utf-8"?>
<TextView xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/text"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:gravity="center"/>
Then in the activity:
public class MainActivity extends AppCompatActivity {
private static final int spanCount = 5;
private static final int totalItemCount = 15;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
RecyclerView recyclerView = findViewById(R.id.recycler);
// Shamelessly stolen from devunwired bit.ly/2yCqVIp
recyclerView.addItemDecoration(new GridDividerDecoration(this));
recyclerView.setLayoutManager(new GridLayoutManager(this, spanCount, LinearLayoutManager.VERTICAL, false));
recyclerView.setAdapter(new MyAdapter(totalItemCount));
}
static class MyAdapter extends RecyclerView.Adapter<MyViewHolder> {
private final int totalSizeOfItems;
private boolean hasBeenSetup = false;
MyAdapter(int totalSizeOfItems) {
this.totalSizeOfItems = totalSizeOfItems;
}
@Override
public MyViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
View view = LayoutInflater.from(parent.getContext()).inflate(R.layout.item, parent, false);
final int cellSize = parent.getMeasuredWidth() / spanCount;
view.setMinimumWidth(cellSize);
view.setMinimumHeight(cellSize);
setupRecyclerHeightIfNeeded(parent, cellSize);
return new MyViewHolder(view);
}
// We need to perform this operation once, not each time `onCreateViewHolder` is called
private void setupRecyclerHeightIfNeeded(View parent, int cellSize) {
if (hasBeenSetup) return;
hasBeenSetup = true;
ViewGroup.MarginLayoutParams params = (ViewGroup.MarginLayoutParams) parent.getLayoutParams();
int numOfRows = (int) (totalItemCount / (double) spanCount);
params.height = numOfRows * cellSize;
new Handler().post(parent::requestLayout);
}
@Override
public void onBindViewHolder(MyViewHolder holder, int pos) {
int position = holder.getAdapterPosition() % totalSizeOfItems;
holder.textView.setText(Integer.toString(position + 1));
}
@Override
public int getItemCount() {
// this will result the list to be "infinite"
return Integer.MAX_VALUE;
}
}
static class MyViewHolder extends RecyclerView.ViewHolder {
TextView textView;
MyViewHolder(View itemView) {
super(itemView);
textView = (TextView) itemView;
}
}
}
On output you'll get:
Taking care of the horizontal scroll would require a small amount of changes:
- orientation of
GridLayoutManager
should be changed to HORIZONTAL
- inside adapter appropriate width/height setters should be substituted
Other than that - everything should be similar.
This solution is based on @azizbekian source code (thanks for interesting example). It was really fun to me to find something close to what you asked (both at the same time). So, here is my modified code:
public class MainActivity extends AppCompatActivity {
private static final int spanCount = 5;
private static final int totalItemCount = 15;
private GridLayoutManager gridLayoutManager;
private RecyclerView recyclerView;
private int orientation = 1;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
recyclerView = (RecyclerView) findViewById(R.id.recycler);
recyclerView.setAdapter(new MyAdapter(totalItemCount));
// Shamelessly stolen from devunwired bit.ly/2yCqVIp
recyclerView.addItemDecoration(new GridDividerDecoration(this));
gridLayoutManager = new GridLayoutManager(this, spanCount);
recyclerView.setLayoutManager(gridLayoutManager);
RecyclerView.OnScrollListener listener = new RecyclerView.OnScrollListener() {
@Override
public void onScrolled(RecyclerView recycler, int dx, int dy) {
super.onScrolled(recycler, dx, dy);
if (dx > 0) {
orientation = LinearLayoutManager.HORIZONTAL;
} else if (dx < 0) {
orientation = LinearLayoutManager.VERTICAL;
} else if (dy > 0) {
orientation = LinearLayoutManager.VERTICAL;
} else if (dy < 0) {
orientation = LinearLayoutManager.HORIZONTAL;
}
recycler.post(new Runnable() {
@Override
public void run() {
gridLayoutManager.setOrientation(orientation);
recyclerView.setLayoutManager(gridLayoutManager);
recyclerView.getAdapter().notifyDataSetChanged();
}
});
}
};
recyclerView.addOnScrollListener(listener);
}
static class MyAdapter extends RecyclerView.Adapter<MyViewHolder> {
private final int totalSizeOfItems;
private boolean hasBeenSetup = false;
MyAdapter(int totalSizeOfItems) {
this.totalSizeOfItems = totalSizeOfItems;
}
@Override
public MyViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
View view = LayoutInflater.from(parent.getContext()).inflate(R.layout.item, parent, false);
final int cellSize = parent.getMeasuredWidth() / spanCount;
view.setMinimumWidth(cellSize);
view.setMinimumHeight(cellSize);
setupRecyclerHeightIfNeeded(parent, cellSize);
return new MyViewHolder(view);
}
// We need to perform this operation once, not each time `onCreateViewHolder` is called
private void setupRecyclerHeightIfNeeded(final View parent, int cellSize) {
if (hasBeenSetup) return;
hasBeenSetup = true;
ViewGroup.MarginLayoutParams params = (ViewGroup.MarginLayoutParams) parent.getLayoutParams();
int numOfRows = (int) (totalItemCount / (double) spanCount);
params.height = numOfRows * cellSize + 100; // modified based on my phone height
new Handler().post(new Runnable() {
@Override
public void run() {
parent.requestLayout();
}
});
}
@Override
public void onBindViewHolder(MyViewHolder holder, int pos) {
int position = holder.getAdapterPosition() % totalSizeOfItems;
holder.textView.setText(Integer.toString(position + 1));
}
@Override
public int getItemCount() {
// this will result the list to be "infinite"
return Integer.MAX_VALUE;
}
}
static class MyViewHolder extends RecyclerView.ViewHolder {
TextView textView;
MyViewHolder(View itemView) {
super(itemView);
textView = (TextView) itemView;
}
}
}
The layout scrolls up for vertical orientation and scrolls left for horizontal. Output is following (sorry for ugly animation):
Hope it will help you.