package com.forcelain.awesomelayoutmanager;
import android.animation.Animator;
import android.animation.ValueAnimator;
import android.graphics.PointF;
import android.graphics.Rect;
import android.support.annotation.Nullable;
import android.support.v7.widget.LinearSmoothScroller;
import android.support.v7.widget.RecyclerView;
import android.util.SparseArray;
import android.view.View;
import android.view.ViewGroup;
import java.util.ArrayList;
public class AwesomeLayoutManager extends RecyclerView.LayoutManager {
public enum Orientation {VERTICAL, HORIZONTAL}
private static final float SCALE_THRESHOLD_PERCENT = 0.66f;
private static final int TRANSITION_DURATION_MS = 400;
private static final float ITEM_HEIGHT_PERCENT = 0.75f;
private RecyclerView recyclerView;
private int scrollStartPos;
private SparseArray<View> viewCache = new SparseArray<>();
private Orientation orientation = Orientation.VERTICAL;
private int anchorPos;
private boolean pagination;
private float scaleThreshold = SCALE_THRESHOLD_PERCENT;
private float pageHeightFactor = ITEM_HEIGHT_PERCENT;
private int transitionDuration = TRANSITION_DURATION_MS;
private int offScreenPages = 0;
/**
* @see #setTransitionDuration(int)
* @return animation's duration in milliseconds
*/
public int getTransitionDuration() {
return transitionDuration;
}
/**
* Set the duration of animated transition between vertical and horizontal {@link Orientation}
* @param transitionDuration animation's duration in milliseconds (400 by default)
*/
public void setTransitionDuration(int transitionDuration) {
this.transitionDuration = transitionDuration;
}
/**
* @see #setPagination(boolean)
* @return true if the AwesomeLayoutManager acts like a ViewPager,
* false if the AwesomeLayoutManager acts like a ListView
*/
public boolean isPagination() {
return pagination;
}
/**
* Set the AwesomeLayoutManager to act like a ViewPager or like a ListView
* @param pagination true is for ViewPager behavior, false otherwise
*/
public void setPagination(boolean pagination) {
this.pagination = pagination;
}
/**
* @see #setScaleFactor(float)
* @return current scale factor
*/
public float getScaleFactor() {
return 1 - scaleThreshold;
}
/**
* Set how much incoming views are scaled
* @param scaleFactor is [0..1]. 0 - not scaled, 1 - maximum scaled
*/
public void setScaleFactor(float scaleFactor) {
this.scaleThreshold = 1 - scaleFactor;
}
/**
* @see #setPageHeightFactor(float)
* @return current page height factor
*/
public float getPageHeightFactor() {
return pageHeightFactor;
}
/**
* Set the maximum page height as a percentage of the RecyclerView's height
* @param pageHeightFactor in (0, 1]. 0.5 is for half-height, 1 is for full RecyclerView's height
*/
public void setPageHeightFactor(float pageHeightFactor) {
this.pageHeightFactor = pageHeightFactor;
}
public Orientation getOrientation() {
return orientation;
}
/**
* Change the orientation immediately (without animation)
* @param orientation The {@link Orientation} to use
*/
public void setOrientation(Orientation orientation) {
View anchorView = getAnchorView();
anchorPos = anchorView != null ? getPosition(anchorView) : 0;
if (orientation != null) {
this.orientation = orientation;
}
requestLayout();
}
/**
* Animated expand the page at the given position and change the orientation to Orientation.HORIZONTAL
* @param pos Adapter's position to open
*/
public void openItem(int pos) {
if (orientation == Orientation.VERTICAL) {
View viewToOpen = null;
int childCount = getChildCount();
for (int i = 0; i < childCount; i++) {
View view = getChildAt(i);
int position = getPosition(view);
if (position == pos) {
viewToOpen = view;
}
}
if (viewToOpen != null) {
openView(viewToOpen);
}
}
}
/**
* Animated collapse current page and change the orientation to Orientation.VERTICAL
*/
public void close() {
View targetView = getAnchorView();
final ArrayList<ViewAnimationInfo> animationInfos = new ArrayList<>();
int childCount = getChildCount();
int targetPos = getPosition(targetView);
for (int i = 0; i < childCount; i++) {
View view = getChildAt(i);
int pos = getPosition(view);
if (pos < targetPos) {
continue;
}
int posDelta = pos - targetPos;
final ViewAnimationInfo viewAnimationInfo = new ViewAnimationInfo();
int maxHeight = (int) (getHeight() * pageHeightFactor);
if (animationInfos.isEmpty()) {
viewAnimationInfo.startTop = getDecoratedTop(view);
viewAnimationInfo.startBottom = getDecoratedBottom(view);
viewAnimationInfo.finishTop = viewAnimationInfo.startTop;
viewAnimationInfo.finishBottom = viewAnimationInfo.finishTop + Math.min(maxHeight, getDecoratedMeasuredHeight(view));
} else {
ViewAnimationInfo prevViewInfo = animationInfos.get(animationInfos.size() - 1);
viewAnimationInfo.startTop = getHeight() * posDelta;
viewAnimationInfo.startBottom = viewAnimationInfo.startTop + getHeight();
viewAnimationInfo.finishTop = prevViewInfo.finishBottom;
viewAnimationInfo.finishBottom = viewAnimationInfo.finishTop + Math.min(maxHeight, getDecoratedMeasuredHeight(view));
}
viewAnimationInfo.view = view;
animationInfos.add(viewAnimationInfo);
}
ValueAnimator animator = ValueAnimator.ofFloat(0, 1);
animator.setDuration(transitionDuration);
animator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
@Override
public void onAnimationUpdate(ValueAnimator animation) {
float animationProgress = (float) animation.getAnimatedValue();
for (ViewAnimationInfo animationInfo : animationInfos) {
int top = (int) (animationInfo.startTop + animationProgress * (animationInfo.finishTop - animationInfo.startTop));
int bottom = (int) (animationInfo.startBottom + animationProgress * (animationInfo.finishBottom - animationInfo.startBottom));
layoutDecorated(animationInfo.view, 0, top, getWidth(), bottom);
notifyChildState(animationInfo.view, 1 - animationProgress);
}
updateViewScale();
}
});
animator.addListener(new Animator.AnimatorListener() {
@Override
public void onAnimationStart(Animator animation) {
}
@Override
public void onAnimationEnd(Animator animation) {
setOrientation(Orientation.VERTICAL);
}
@Override
public void onAnimationCancel(Animator animation) {
}
@Override
public void onAnimationRepeat(Animator animation) {
}
});
animator.start();
}
@Override
public void onAttachedToWindow(final RecyclerView recyclerView) {
super.onAttachedToWindow(recyclerView);
this.recyclerView = recyclerView;
recyclerView.setChildDrawingOrderCallback(new RecyclerView.ChildDrawingOrderCallback() {
@Override
public int onGetChildDrawingOrder(int childCount, int i) {
return childCount - i - 1;
}
});
recyclerView.setOnFlingListener(new RecyclerView.OnFlingListener() {
@Override
public boolean onFling(int velocityX, int velocityY) {
if (!pagination) {
return false;
}
int position = scrollStartPos;
int velocity = 0;
switch (orientation) {
case VERTICAL:
velocity = velocityY;
break;
case HORIZONTAL:
velocity = velocityX;
break;
}
position = velocity > 0 ? position + 1 : position - 1;
position = Math.max(position, 0);
position = Math.min(position, getItemCount() - 1);
recyclerView.smoothScrollToPosition(position);
return true;
}
});
}
@Override
public void onDetachedFromWindow(RecyclerView view, RecyclerView.Recycler recycler) {
super.onDetachedFromWindow(view, recycler);
recyclerView = null;
}
@Override
public RecyclerView.LayoutParams generateDefaultLayoutParams() {
return new RecyclerView.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT);
}
@Override
public void onLayoutChildren(RecyclerView.Recycler recycler, RecyclerView.State state) {
detachAndScrapAttachedViews(recycler);
fill(recycler);
anchorPos = -1;
}
@Override
public void onScrollStateChanged(int state) {
super.onScrollStateChanged(state);
if (!pagination) {
return;
}
if (state == RecyclerView.SCROLL_STATE_DRAGGING) {
scrollStartPos = getPosition(getAnchorView());
}
if (state == RecyclerView.SCROLL_STATE_IDLE) {
switch (orientation) {
case VERTICAL:
checkLayoutVertical();
break;
case HORIZONTAL:
checkLayoutHorizontal();
break;
}
}
}
private void checkLayoutHorizontal() {
View anchorView = getAnchorView();
int left = getDecoratedLeft(anchorView);
if (left != 0) {
recyclerView.smoothScrollBy(left, 0);
}
}
private void checkLayoutVertical() {
View anchorView = getAnchorView();
int position = getPosition(anchorView);
if (position != 0 && position == getItemCount() - 1) {
int bottom = getDecoratedBottom(anchorView);
if (bottom != getHeight()) {
recyclerView.smoothScrollBy(0, bottom);
}
} else {
int decoratedTop = getDecoratedTop(anchorView);
if (decoratedTop != 0) {
recyclerView.smoothScrollBy(0, decoratedTop);
}
}
}
protected void openView(final View targetView) {
final ArrayList<ViewAnimationInfo> animationInfos = new ArrayList<>();
int childCount = getChildCount();
int targetPos = getPosition(targetView);
for (int i = 0; i < childCount; i++) {
View view = getChildAt(i);
int pos = getPosition(view);
int posDelta = pos - targetPos;
final ViewAnimationInfo viewAnimationInfo = new ViewAnimationInfo();
viewAnimationInfo.startTop = getDecoratedTop(view);
viewAnimationInfo.startBottom = getDecoratedBottom(view);
viewAnimationInfo.finishTop = getHeight() * posDelta;
viewAnimationInfo.finishBottom = viewAnimationInfo.finishTop + getDecoratedMeasuredHeight(targetView);
viewAnimationInfo.view = view;
animationInfos.add(viewAnimationInfo);
}
ValueAnimator animator = ValueAnimator.ofFloat(0, 1);
animator.setDuration(transitionDuration);
animator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
@Override
public void onAnimationUpdate(ValueAnimator animation) {
float animationProgress = (float) animation.getAnimatedValue();
for (ViewAnimationInfo animationInfo : animationInfos) {
int top = (int) (animationInfo.startTop + animationProgress * (animationInfo.finishTop - animationInfo.startTop));
int bottom = (int) (animationInfo.startBottom + animationProgress * (animationInfo.finishBottom - animationInfo.startBottom));
layoutDecorated(animationInfo.view, 0, top, getWidth(), bottom);
notifyChildState(animationInfo.view, animationProgress);
}
updateViewScale();
}
});
animator.addListener(new Animator.AnimatorListener() {
@Override
public void onAnimationStart(Animator animation) {
}
@Override
public void onAnimationEnd(Animator animation) {
setOrientation(Orientation.HORIZONTAL);
}
@Override
public void onAnimationCancel(Animator animation) {
}
@Override
public void onAnimationRepeat(Animator animation) {
}
});
animator.start();
}
private float map(float x1, float x2, float y1, float y2, float n) {
return (n - x1) * (y2 - y1) / (x2 - x1) + y1;
}
private void fill(RecyclerView.Recycler recycler) {
offScreenPages = (int) (getHeight() / pageHeightFactor);
View anchorView = getAnchorView();
viewCache.clear();
for (int i = 0, cnt = getChildCount(); i < cnt; i++) {
View view = getChildAt(i);
int pos = getPosition(view);
viewCache.put(pos, view);
}
for (int i = 0; i < viewCache.size(); i++) {
detachView(viewCache.valueAt(i));
}
switch (orientation) {
case VERTICAL:
fillUp(anchorView, recycler);
fillDown(anchorView, recycler);
break;
case HORIZONTAL:
fillLeft(anchorView, recycler);
fillRight(anchorView, recycler);
break;
}
for (int i = 0; i < viewCache.size(); i++) {
recycler.recycleView(viewCache.valueAt(i));
}
updateViewScale();
}
private void fillUp(@Nullable View anchorView, RecyclerView.Recycler recycler) {
int anchorPos;
int anchorTop = 0;
if (this.anchorPos >= 0) {
anchorPos = this.anchorPos;
} else if (anchorView == null) {
anchorPos = 0;
} else {
anchorPos = getPosition(anchorView);
anchorTop = getDecoratedTop(anchorView);
}
boolean fillUp = true;
int pos = anchorPos - 1;
int viewBottom = anchorTop;
int viewHeight = (int) (getHeight() * pageHeightFactor);
final int widthSpec = View.MeasureSpec.makeMeasureSpec(getWidth(), View.MeasureSpec.EXACTLY);
final int heightSpec = View.MeasureSpec.makeMeasureSpec(getHeight(), View.MeasureSpec.AT_MOST);
while (fillUp && pos >= 0) {
View view = viewCache.get(pos);
if (view == null) {
view = recycler.getViewForPosition(pos);
addView(view, 0);
measureChildWithDecorationsAndMargin(view, widthSpec, heightSpec);
int decoratedMeasuredWidth = getDecoratedMeasuredWidth(view);
layoutDecorated(view, 0, viewBottom - Math.min(viewHeight, getDecoratedMeasuredHeight(view)), decoratedMeasuredWidth, viewBottom);
} else {
attachView(view, 0);
viewCache.remove(pos);
}
notifyChildState(view, 0);
viewBottom = getDecoratedTop(view);
fillUp = (viewBottom > 0);
pos--;
}
}
private void fillDown(@Nullable View anchorView, RecyclerView.Recycler recycler) {
int anchorPos;
int anchorTop = 0;
if (this.anchorPos >= 0) {
anchorPos = this.anchorPos;
} else if (anchorView == null) {
anchorPos = 0;
} else {
anchorPos = getPosition(anchorView);
anchorTop = getDecoratedTop(anchorView);
}
int pos = anchorPos;
boolean fillDown = true;
int height = getHeight();
int viewTop = anchorTop;
int itemCount = getItemCount();
int viewHeight = (int) (getHeight() * pageHeightFactor);
final int widthSpec = View.MeasureSpec.makeMeasureSpec(getWidth(), View.MeasureSpec.EXACTLY);
final int heightSpec = View.MeasureSpec.makeMeasureSpec(getHeight(), View.MeasureSpec.AT_MOST);
while (fillDown && pos < itemCount) {
View view = viewCache.get(pos);
if (view == null) {
view = recycler.getViewForPosition(pos);
addView(view);
measureChildWithDecorationsAndMargin(view, widthSpec, heightSpec);
int decoratedMeasuredWidth = getDecoratedMeasuredWidth(view);
layoutDecorated(view, 0, viewTop, decoratedMeasuredWidth, viewTop + Math.min(viewHeight, getDecoratedMeasuredHeight(view)));
} else {
attachView(view);
viewCache.remove(pos);
}
notifyChildState(view, 0);
viewTop = getDecoratedBottom(view);
fillDown = viewTop <= height;
pos++;
}
}
private void fillLeft(@Nullable View anchorView, RecyclerView.Recycler recycler) {
int anchorPos;
int anchorLeft = 0;
if (this.anchorPos >= 0) {
anchorPos = this.anchorPos;
} else if (anchorView == null) {
anchorPos = 0;
} else {
anchorPos = getPosition(anchorView);
anchorLeft = getDecoratedLeft(anchorView);
}
int pos = anchorPos - 1;
int nextViewRight = anchorLeft;
int width = getWidth();
boolean fillLeft = canFillLeft(nextViewRight, width);
int height = getHeight();
final int widthSpec = View.MeasureSpec.makeMeasureSpec(width, View.MeasureSpec.EXACTLY);
final int heightSpec = View.MeasureSpec.makeMeasureSpec(height, View.MeasureSpec.AT_MOST);
while (fillLeft && pos >= 0) {
View view = viewCache.get(pos);
if (view == null) {
view = recycler.getViewForPosition(pos);
addView(view, 0);
measureChildWithDecorationsAndMargin(view, widthSpec, heightSpec);
int decoratedMeasuredHeight = getDecoratedMeasuredHeight(view);
int decoratedMeasuredWidth = getDecoratedMeasuredWidth(view);
layoutDecorated(view, nextViewRight - decoratedMeasuredWidth, 0, nextViewRight, decoratedMeasuredHeight);
} else {
attachView(view);
viewCache.remove(pos);
}
notifyChildState(view, 1);
nextViewRight = getDecoratedLeft(view);
fillLeft = canFillLeft(nextViewRight, width);
pos--;
}
}
private boolean canFillLeft(int nextViewRight, int width) {
return nextViewRight > -width * offScreenPages;
}
private void fillRight(View anchorView, RecyclerView.Recycler recycler) {
int anchorPos;
int anchorLeft = 0;
if (this.anchorPos >= 0) {
anchorPos = this.anchorPos;
} else if (anchorView == null) {
anchorPos = 0;
} else {
anchorPos = getPosition(anchorView);
anchorLeft = getDecoratedLeft(anchorView);
}
int pos = anchorPos;
boolean fillRight = true;
int nextViewLeft = anchorLeft;
int itemCount = getItemCount();
int width = getWidth();
int height = getHeight();
final int widthSpec = View.MeasureSpec.makeMeasureSpec(width, View.MeasureSpec.EXACTLY);
final int heightSpec = View.MeasureSpec.makeMeasureSpec(height, View.MeasureSpec.AT_MOST);
while (fillRight && pos < itemCount) {
View view = viewCache.get(pos);
if (view == null) {
view = recycler.getViewForPosition(pos);
addView(view);
measureChildWithDecorationsAndMargin(view, widthSpec, heightSpec);
int decoratedMeasuredHeight = getDecoratedMeasuredHeight(view);
int decoratedMeasuredWidth = getDecoratedMeasuredWidth(view);
layoutDecorated(view, nextViewLeft, 0, nextViewLeft + decoratedMeasuredWidth, decoratedMeasuredHeight);
} else {
attachView(view);
viewCache.remove(pos);
}
notifyChildState(view, 1);
nextViewLeft = getDecoratedRight(view);
fillRight = nextViewLeft < width + width * offScreenPages;
pos++;
}
}
private void notifyChildState(View view, float progress) {
RecyclerView.ViewHolder childViewHolder = recyclerView.getChildViewHolder(view);
if (childViewHolder instanceof AwesomeViewHolder) {
((AwesomeViewHolder) childViewHolder).onStateChanged(progress);
}
}
private void updateViewScale() {
int childCount = getChildCount();
int height = getHeight();
int thresholdPerc = (int) (height * scaleThreshold);
for (int i = 0; i < childCount; i++) {
float scale = 1f;
View view = getChildAt(i);
int viewTop = getDecoratedTop(view);
if (viewTop >= thresholdPerc) {
int delta = viewTop - thresholdPerc;
scale = (height - delta) / (float) height;
scale = Math.max(scale, 0);
}
int pivotY = (int) map(thresholdPerc, height, getHeight() / -2, 0, viewTop);
view.setPivotX(view.getWidth() / 2);
view.setPivotY(pivotY);
view.setScaleX(scale);
view.setScaleY(scale);
}
}
protected View getAnchorView() {
int childCount = getChildCount();
Rect mainRect = new Rect(0, 0, getWidth(), getHeight());
int maxSquare = 0;
View anchorView = null;
for (int i = 0; i < childCount; i++) {
View view = getChildAt(i);
int top = getDecoratedTop(view);
int bottom = getDecoratedBottom(view);
int left = getDecoratedLeft(view);
int right = getDecoratedRight(view);
Rect viewRect = new Rect(left, top, right, bottom);
boolean intersect = viewRect.intersect(mainRect);
if (intersect) {
int square = viewRect.width() * viewRect.height();
if (square > maxSquare) {
maxSquare = square;
anchorView = view;
}
}
}
return anchorView;
}
@Override
public void smoothScrollToPosition(RecyclerView recyclerView, RecyclerView.State state, int position) {
if (position >= getItemCount()) {
return;
}
LinearSmoothScroller scroller = new LinearSmoothScroller(recyclerView.getContext()) {
@Override
public PointF computeScrollVectorForPosition(int targetPosition) {
return AwesomeLayoutManager.this.computeScrollVectorForPosition(targetPosition);
}
@Override
protected int getHorizontalSnapPreference() {
return SNAP_TO_START;
}
@Override
protected int getVerticalSnapPreference() {
return SNAP_TO_START;
}
};
scroller.setTargetPosition(position);
startSmoothScroll(scroller);
}
private PointF computeScrollVectorForPosition(int targetPosition) {
if (getChildCount() == 0) {
return null;
}
final int firstChildPos = getPosition(getChildAt(0));
final int direction = targetPosition < firstChildPos ? -1 : 1;
if (orientation == Orientation.HORIZONTAL) {
return new PointF(direction, 0);
} else {
return new PointF(0, direction);
}
}
@Override
public boolean canScrollVertically() {
return orientation == Orientation.VERTICAL;
}
@Override
public boolean canScrollHorizontally() {
return orientation == Orientation.HORIZONTAL;
}
@Override
public int scrollHorizontallyBy(int dx, RecyclerView.Recycler recycler, RecyclerView.State state) {
int delta = scrollHorizontallyInternal(dx);
offsetChildrenHorizontal(-delta);
fill(recycler);
return delta;
}
@Override
public int scrollVerticallyBy(int dy, RecyclerView.Recycler recycler, RecyclerView.State state) {
int delta = scrollVerticallyInternal(dy);
offsetChildrenVertical(-delta);
fill(recycler);
return delta;
}
@Override
public void scrollToPosition(int position) {
super.scrollToPosition(position);
anchorPos = position;
requestLayout();
}
private int scrollVerticallyInternal(int dy) {
int childCount = getChildCount();
int itemCount = getItemCount();
if (childCount == 0) {
return 0;
}
final View topView = getChildAt(0);
final View bottomView = getChildAt(childCount - 1);
int viewSpan = getDecoratedBottom(bottomView) - getDecoratedTop(topView);
if (viewSpan <= getHeight()) {
return 0;
}
int delta = 0;
if (dy < 0) {
View firstView = getChildAt(0);
int firstViewAdapterPos = getPosition(firstView);
if (firstViewAdapterPos > 0) {
delta = dy;
} else {
int viewTop = getDecoratedTop(firstView);
delta = Math.max(viewTop, dy);
}
} else if (dy > 0) {
View lastView = getChildAt(childCount - 1);
int lastViewAdapterPos = getPosition(lastView);
if (lastViewAdapterPos < itemCount - 1) {
delta = dy;
} else {
int viewBottom = getDecoratedBottom(lastView);
int parentBottom = getHeight();
delta = Math.min(viewBottom - parentBottom, dy);
}
}
return delta;
}
private int scrollHorizontallyInternal(int dx) {
int childCount = getChildCount();
int itemCount = getItemCount();
if (childCount == 0) {
return 0;
}
int delta = 0;
if (dx < 0) {
View firstView = getChildAt(0);
int firstViewAdapterPos = getPosition(firstView);
if (firstViewAdapterPos > 0) {
delta = dx;
} else {
int viewLeft = getDecoratedLeft(firstView);
delta = Math.max(viewLeft, dx);
}
} else if (dx > 0) {
View lastView = getChildAt(childCount - 1);
int lastViewAdapterPos = getPosition(lastView);
if (lastViewAdapterPos < itemCount - 1) {
delta = dx;
} else {
int viewRight = getDecoratedRight(lastView);
delta = Math.min(viewRight - getWidth(), dx);
}
}
return delta;
}
private void measureChildWithDecorationsAndMargin(View child, int widthSpec, int heightSpec) {
Rect decorRect = new Rect();
calculateItemDecorationsForChild(child, decorRect);
RecyclerView.LayoutParams lp = (RecyclerView.LayoutParams) child.getLayoutParams();
widthSpec = updateSpecWithExtra(widthSpec, lp.leftMargin + decorRect.left,
lp.rightMargin + decorRect.right);
heightSpec = updateSpecWithExtra(heightSpec, lp.topMargin + decorRect.top,
lp.bottomMargin + decorRect.bottom);
child.measure(widthSpec, heightSpec);
}
private int updateSpecWithExtra(int spec, int startInset, int endInset) {
if (startInset == 0 && endInset == 0) {
return spec;
}
final int mode = View.MeasureSpec.getMode(spec);
if (mode == View.MeasureSpec.AT_MOST || mode == View.MeasureSpec.EXACTLY) {
return View.MeasureSpec.makeMeasureSpec(
View.MeasureSpec.getSize(spec) - startInset - endInset, mode);
}
return spec;
}
private static class ViewAnimationInfo {
int startTop;
int startBottom;
int finishTop;
int finishBottom;
View view;
}
}