【问题标题】:Infinite scroll of finite items无限项的无限滚动
【发布时间】:2017-11-11 21:27:08
【问题描述】:

我有一个 GridView,里面有 5x50 的项目。

我需要向各个方向滚动它们,而不是在到达终点时停止,而是从顶部/左侧开始。

例如从左到右滚动

滚动前

1 2 3 4 5
6 7 8 9 10

向右滚动后

​​>
5 1 2 3 4
10 6 7 8 9

对于自上而下(或自下而上)

滚动到底部之前

1 2 3 4 5
6 7 8 9 10
11 12 13 14 15

滚动后

​​>
6 7 8 9 10
11 12 13 14 15
1 2 3 4 5

我尝试让它像GridView原生滚动一样平滑滚动。

【问题讨论】:

    标签: android android-layout android-canvas android-view android-gridview


    【解决方案1】:

    在 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>
    

    还有一个基本列表项 - TextView,在 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"/>
    

    然后在活动中:

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

    在输出时你会得到:

    处理水平滚动需要进行少量更改:

    • GridLayoutManager 的方向应更改为HORIZONTAL
    • 应替换适配器内部适当的宽度/高度设置器

    除此之外 - 一切都应该相似。

    【讨论】:

    • 这很完美,我明白需要如何更改,但我需要同时拥有两者
    【解决方案2】:

    此解决方案基于 @azizbekian 源代码(感谢有趣的示例)。找到与您所要求的内容相近的东西对我来说真的很有趣(同时)。所以,这是我修改后的代码:

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

    垂直方向的布局向上滚动,水平方向的布局向左滚动。输出如下(抱歉动画很丑):

    希望对你有所帮助。

    【讨论】:

    • 您是否更改了任何布局或其他文件,例如 gradle?启动活动需要很长时间,然后在尝试滚动后崩溃
    • 不,我没有更改布局中的任何内容,而是修改了源代码。在原始版本中,以下行:new Handler().post(parent::requestLayout); 使用 java8,我遇到了问题,android studio 在我的构建 java 8 支持中添加了但仍然有问题。所以,我只是用这行代码替换了那行代码:new Handler().post(new Runnable() { @Override public void run() { parent.requestLayout(); } });
    • 还在我的项目中添加了GridDividerDecoration.java 用于装饰。
    • 你可以在我的github找到这个项目
    • 发现notification 的问题只需添加查看更新的代码,添加新行:recyclerView.getAdapter().notifyDataSetChanged();
    【解决方案3】:

    Here 是另一种解决方案,但采用了画布方法。

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

    【讨论】:

    • 我想这将是我想要的最接近的东西
    • 点击如何处理?
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 2013-03-09
    • 2017-06-24
    • 2012-10-03
    • 2020-12-05
    • 2015-09-07
    • 2012-05-11
    • 2017-01-25
    相关资源
    最近更新 更多