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