Infinite scroll of finite items
Asked Answered
D

3

16

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.

Doomsday answered 11/11, 2017 at 21:27 Comment(0)
L
3

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);
    }
}
Loathsome answered 19/11, 2017 at 11:54 Comment(2)
I guess this will be closest thing for what I wantedDoomsday
How to deal with clicks ?Santiago
W
13

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:

enter image description here

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.

Wreak answered 14/11, 2017 at 12:41 Comment(1)
This is perfect and I understand how changes needed, but I need to have both at the same timeDoomsday
B
4

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):

enter image description here

Hope it will help you.

Benignity answered 15/11, 2017 at 17:28 Comment(9)
Did you change any layout or some other file like gradle? It takes lots of time to start activity and then after trying to scroll it crashesDoomsday
no, I did not change anything in layouts but modified source code. In original version the following line: new Handler().post(parent::requestLayout); uses java8, I had a problem with it and android studio added in my build java 8 support but still had a problem. So, I just replaced that line of code with this one: new Handler().post(new Runnable() { @Override public void run() { parent.requestLayout(); } });Benignity
also added GridDividerDecoration.java into my project for decoration purpose.Benignity
you can find this project in my githubBenignity
found an issue with notification just add look at the updated code, added new line: recyclerView.getAdapter().notifyDataSetChanged();Benignity
imgur.com/a/AOtQF this is a output I get. I'm using Gradle 3.0 so only thing that I change is buildToolsVersion "26.0.2"Doomsday
from your log output I see that your app is started and you are scrolling recyclerview vertically, try to scroll it horizontally too. Log message with I/TAG is shown when you scroll the list. Do you have any difficulties with running app? I have modified the code one time in github so if you did not updated just do it.Benignity
This is all the output it gave after long time of starting and single touch. Which was not displayed due to crash. I was able to finally run the code after changing Integer.MAX_VALUE to 1000. While it is working this is not what I had in my mind. I need all direction scrolling and smoother than this since this changes sizes and is irresponsible when changing direction. I guess it has to be done with canvas rather then layout as I'm looking at it. I'm sorry for your trouble but I cannot accept this answer.Doomsday
no problem, it's fun and let's look at better solution!Benignity
L
3

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);
    }
}
Loathsome answered 19/11, 2017 at 11:54 Comment(2)
I guess this will be closest thing for what I wantedDoomsday
How to deal with clicks ?Santiago

© 2022 - 2024 — McMap. All rights reserved.