Infinite scroll of finite items

2019-03-11 02:22发布

问题:

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.

回答1:

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


回答2:

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.



回答3:

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.