package org.geeksforgeeks.demo; import android.graphics.Rect; import android.util.ArrayMap; import android.util.SparseArray; import android.util.SparseBooleanArray; import android.view.View; import android.view.ViewGroup; import androidx.recyclerview.widget.RecyclerView; /** * A custom RecyclerView LayoutManager that arranges items vertically * with scale and fade animations as they scroll in and out of view. */ public class CustomLayoutManager extends RecyclerView.LayoutManager { private int scroll = 0; // Current scroll position private final SparseArray<Rect> locationRects = new SparseArray<>(); // Stores bounds of all items private final SparseBooleanArray attachedItems = new SparseBooleanArray(); // Tracks which views are currently attached private final ArrayMap<Integer, Integer> viewTypeHeightMap = new ArrayMap<>(); // Caches item heights per viewType private boolean needSnap = false; // Whether we need to snap after scroll private int lastDy = 0; // Last scroll delta private int maxScroll = -1; // Maximum scroll value private RecyclerView.Adapter adapter; // Adapter reference private RecyclerView.Recycler recycler; // Recycler reference public CustomLayoutManager() { setAutoMeasureEnabled(true); // Enables auto measuring of child views } @Override public RecyclerView.LayoutParams generateDefaultLayoutParams() { return new RecyclerView.LayoutParams( ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT ); } @Override public void onAdapterChanged(RecyclerView.Adapter oldAdapter, RecyclerView.Adapter newAdapter) { super.onAdapterChanged(oldAdapter, newAdapter); this.adapter = newAdapter; } @Override public void onLayoutChildren(RecyclerView.Recycler recycler, RecyclerView.State state) { this.recycler = recycler; // Skip layout during pre-layout phase if (state.isPreLayout()) return; // Rebuild item layout information buildLocationRects(); // Remove and recycle all current views detachAndScrapAttachedViews(recycler); // Layout the initial visible views layoutItemsOnCreate(recycler); } /** * Builds position and size data for all items. */ private void buildLocationRects() { locationRects.clear(); attachedItems.clear(); int tempPosition = getPaddingTop(); int itemCount = getItemCount(); for (int i = 0; i < itemCount; i++) { int viewType = adapter.getItemViewType(i); int itemHeight; // Check height cache for view type if (viewTypeHeightMap.containsKey(viewType)) { itemHeight = viewTypeHeightMap.get(viewType); } else { // Measure item to get height View itemView = recycler.getViewForPosition(i); addView(itemView); measureChildWithMargins(itemView, View.MeasureSpec.UNSPECIFIED, View.MeasureSpec.UNSPECIFIED); itemHeight = getDecoratedMeasuredHeight(itemView); viewTypeHeightMap.put(viewType, itemHeight); } // Record layout rectangle for the item Rect rect = new Rect(); rect.left = getPaddingLeft(); rect.top = tempPosition; rect.right = getWidth() - getPaddingRight(); rect.bottom = rect.top + itemHeight; locationRects.put(i, rect); attachedItems.put(i, false); tempPosition += itemHeight; } // Calculate maximum scroll distance maxScroll = itemCount == 0 ? 0 : computeMaxScroll(); } public int findFirstVisibleItemPosition() { int count = locationRects.size(); Rect displayRect = new Rect(0, scroll, getWidth(), getHeight() + scroll); for (int i = 0; i < count; i++) { if (Rect.intersects(displayRect, locationRects.get(i)) && attachedItems.get(i)) { return i; } } return 0; } private int computeMaxScroll() { int max = locationRects.get(locationRects.size() - 1).bottom - getHeight(); if (max < 0) return 0; // Add extra height for snap if items partially fill screen int screenFilledHeight = 0; for (int i = getItemCount() - 1; i >= 0; i--) { Rect rect = locationRects.get(i); screenFilledHeight += (rect.bottom - rect.top); if (screenFilledHeight > getHeight()) { int extraSnapHeight = getHeight() - (screenFilledHeight - (rect.bottom - rect.top)); max += extraSnapHeight; break; } } return max; } /** * Lays out views during initial layout (onCreate). */ private void layoutItemsOnCreate(RecyclerView.Recycler recycler) { int itemCount = getItemCount(); Rect displayRect = new Rect(0, scroll, getWidth(), getHeight() + scroll); for (int i = 0; i < itemCount; i++) { Rect thisRect = locationRects.get(i); if (Rect.intersects(displayRect, thisRect)) { View childView = recycler.getViewForPosition(i); addView(childView); measureChildWithMargins(childView, View.MeasureSpec.UNSPECIFIED, View.MeasureSpec.UNSPECIFIED); layoutItem(childView, thisRect); attachedItems.put(i, true); childView.setPivotY(0); childView.setPivotX(childView.getMeasuredWidth() / 2f); // Stop laying out if beyond screen height if (thisRect.top - scroll > getHeight()) break; } } } /** * Handles layout during scrolling. */ private void layoutItemsOnScroll() { int childCount = getChildCount(); int itemCount = getItemCount(); Rect displayRect = new Rect(0, scroll, getWidth(), getHeight() + scroll); int firstVisiblePosition = -1, lastVisiblePosition = -1; // Remove views out of bounds, update bounds of visible ones for (int i = childCount - 1; i >= 0; i--) { View child = getChildAt(i); if (child == null) continue; int position = getPosition(child); if (!Rect.intersects(displayRect, locationRects.get(position))) { removeAndRecycleView(child, recycler); attachedItems.put(position, false); } else { if (lastVisiblePosition < 0) lastVisiblePosition = position; if (firstVisiblePosition < 0) firstVisiblePosition = position; else firstVisiblePosition = Math.min(firstVisiblePosition, position); layoutItem(child, locationRects.get(position)); } } // Add views before and after visible range if (firstVisiblePosition > 0) { for (int i = firstVisiblePosition - 1; i >= 0; i--) { if (Rect.intersects(displayRect, locationRects.get(i)) && !attachedItems.get(i)) { reuseItemOnSroll(i, true); } else break; } } for (int i = lastVisiblePosition + 1; i < itemCount; i++) { if (Rect.intersects(displayRect, locationRects.get(i)) && !attachedItems.get(i)) { reuseItemOnSroll(i, false); } else break; } } /** * Reuses a view at the given position and attaches it to layout. */ private void reuseItemOnSroll(int position, boolean addViewFromTop) { View scrap = recycler.getViewForPosition(position); measureChildWithMargins(scrap, 0, 0); scrap.setPivotY(0); scrap.setPivotX(scrap.getMeasuredWidth() / 2f); if (addViewFromTop) addView(scrap, 0); else addView(scrap); layoutItem(scrap, locationRects.get(position)); attachedItems.put(position, true); } /** * Lays out a single child view and applies scale/alpha transformations. */ private void layoutItem(View child, Rect rect) { int topDistance = scroll - rect.top; int layoutTop, layoutBottom; int itemHeight = rect.bottom - rect.top; if (topDistance > 0 && topDistance < itemHeight) { float rate1 = (float) topDistance / itemHeight; float rate2 = 1 - rate1 * rate1 / 3; float rate3 = 1 - rate1 * rate1; child.setScaleX(rate2); child.setScaleY(rate2); child.setAlpha(rate3); layoutTop = 0; layoutBottom = itemHeight; } else { child.setScaleX(1); child.setScaleY(1); child.setAlpha(1); layoutTop = rect.top - scroll; layoutBottom = rect.bottom - scroll; } layoutDecorated(child, rect.left, layoutTop, rect.right, layoutBottom); } @Override public boolean canScrollVertically() { return true; } @Override public int scrollVerticallyBy(int dy, RecyclerView.Recycler recycler, RecyclerView.State state) { if (getItemCount() == 0 || dy == 0) return 0; int travel = dy; if (scroll + dy < 0) { travel = -scroll; } else if (scroll + dy > maxScroll) { travel = maxScroll - scroll; } scroll += travel; lastDy = dy; if (!state.isPreLayout() && getChildCount() > 0) { layoutItemsOnScroll(); } return travel; } @Override public void onAttachedToWindow(RecyclerView view) { super.onAttachedToWindow(view); new StartSnapHelper().attachToRecyclerView(view); // Attach custom snap helper } @Override public void onScrollStateChanged(int state) { if (state == RecyclerView.SCROLL_STATE_DRAGGING) { needSnap = true; } super.onScrollStateChanged(state); } /** * Returns the amount needed to scroll for a snap-to-position effect. */ public int getSnapHeight() { if (!needSnap) return 0; needSnap = false; Rect displayRect = new Rect(0, scroll, getWidth(), getHeight() + scroll); int itemCount = getItemCount(); for (int i = 0; i < itemCount; i++) { Rect itemRect = locationRects.get(i); if (displayRect.intersect(itemRect)) { if (lastDy > 0 && i < itemCount - 1) { Rect nextRect = locationRects.get(i + 1); return nextRect.top - displayRect.top; } return itemRect.top - displayRect.top; } } return 0; } /** * Returns the first visible view to be used for snapping. */ public View findSnapView() { if (getChildCount() > 0) { return getChildAt(0); } return null; } }