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);
}
}