package com.loopeer.cardstack;
import android.annotation.TargetApi;
import android.content.Context;
import android.content.res.TypedArray;
import android.database.Observable;
import android.os.Build;
import android.util.AttributeSet;
import android.util.Log;
import android.view.MotionEvent;
import android.view.VelocityTracker;
import android.view.View;
import android.view.ViewConfiguration;
import android.view.ViewGroup;
import android.view.ViewParent;
import android.widget.OverScroller;
import java.util.ArrayList;
import java.util.List;
public class CardStackView extends ViewGroup implements ScrollDelegate {
private static final int INVALID_POINTER = -1;
public static final int INVALID_TYPE = -1;
public static final int ANIMATION_STATE_START = 0;
public static final int ANIMATION_STATE_END = 1;
public static final int ANIMATION_STATE_CANCEL = 2;
private static final String TAG = "CardStackView";
public static final int ALL_DOWN = 0;
public static final int UP_DOWN = 1;
public static final int UP_DOWN_STACK = 2;
static final int DEFAULT_SELECT_POSITION = -1;
private int mTotalLength;
private int mOverlapGaps;
private int mOverlapGapsCollapse;
private int mNumBottomShow;
private StackAdapter mStackAdapter;
private final ViewDataObserver mObserver = new ViewDataObserver();
private int mSelectPosition = DEFAULT_SELECT_POSITION;
private int mShowHeight;
private List<ViewHolder> mViewHolders;
private AnimatorAdapter mAnimatorAdapter;
private int mDuration;
private OverScroller mScroller;
private int mLastMotionY;
private boolean mIsBeingDragged = false;
private VelocityTracker mVelocityTracker;
private int mTouchSlop;
private int mMinimumVelocity;
private int mMaximumVelocity;
private int mActivePointerId = INVALID_POINTER;
private final int[] mScrollOffset = new int[2];
private int mNestedYOffset;
private boolean mScrollEnable = true;
private ScrollDelegate mScrollDelegate;
private ItemExpendListener mItemExpendListener;
public CardStackView(Context context) {
this(context, null);
}
public CardStackView(Context context, AttributeSet attrs) {
this(context, attrs, 0);
}
public CardStackView(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
init(context, attrs, defStyleAttr, 0);
}
@TargetApi(Build.VERSION_CODES.LOLLIPOP)
public CardStackView(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
super(context, attrs, defStyleAttr, defStyleRes);
init(context, attrs, defStyleAttr, defStyleRes);
}
private void init(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
TypedArray array = context.obtainStyledAttributes(attrs, R.styleable.CardStackView, defStyleAttr, defStyleRes);
setOverlapGaps(array.getDimensionPixelSize(R.styleable.CardStackView_stackOverlapGaps, dp2px(20)));
setOverlapGapsCollapse(array.getDimensionPixelSize(R.styleable.CardStackView_stackOverlapGapsCollapse, dp2px(20)));
setDuration(array.getInt(R.styleable.CardStackView_stackDuration, AnimatorAdapter.ANIMATION_DURATION));
setAnimationType(array.getInt(R.styleable.CardStackView_stackAnimationType, UP_DOWN_STACK));
setNumBottomShow(array.getInt(R.styleable.CardStackView_stackNumBottomShow, 3));
array.recycle();
mViewHolders = new ArrayList<>();
initScroller();
}
private void initScroller() {
mScroller = new OverScroller(getContext());
setFocusable(true);
setDescendantFocusability(FOCUS_AFTER_DESCENDANTS);
final ViewConfiguration configuration = ViewConfiguration.get(getContext());
mTouchSlop = configuration.getScaledTouchSlop();
mMinimumVelocity = configuration.getScaledMinimumFlingVelocity();
mMaximumVelocity = configuration.getScaledMaximumFlingVelocity();
}
private int dp2px(int value) {
final float scale = getContext().getResources().getDisplayMetrics().density;
return (int) (value * scale + 0.5f);
}
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
checkContentHeightByParent();
measureChild(widthMeasureSpec, heightMeasureSpec);
}
private void checkContentHeightByParent() {
View parentView = (View) getParent();
mShowHeight = parentView.getMeasuredHeight() - parentView.getPaddingTop() - parentView.getPaddingBottom();
}
private void measureChild(int widthMeasureSpec, int heightMeasureSpec) {
int maxWidth = 0;
mTotalLength = 0;
mTotalLength += getPaddingTop() + getPaddingBottom();
for (int i = 0; i < getChildCount(); i++) {
final View child = getChildAt(i);
measureChildWithMargins(child, widthMeasureSpec, 0, heightMeasureSpec, 0);
final int totalLength = mTotalLength;
final LayoutParams lp =
(LayoutParams) child.getLayoutParams();
if (lp.mHeaderHeight == -1) lp.mHeaderHeight = child.getMeasuredHeight();
final int childHeight = lp.mHeaderHeight;
mTotalLength = Math.max(totalLength, totalLength + childHeight + lp.topMargin +
lp.bottomMargin);
mTotalLength -= mOverlapGaps * 2;
final int margin = lp.leftMargin + lp.rightMargin;
final int measuredWidth = child.getMeasuredWidth() + margin;
maxWidth = Math.max(maxWidth, measuredWidth);
}
mTotalLength += mOverlapGaps * 2;
int heightSize = mTotalLength;
heightSize = Math.max(heightSize, mShowHeight);
int heightSizeAndState = resolveSizeAndState(heightSize, heightMeasureSpec, 0);
setMeasuredDimension(resolveSizeAndState(maxWidth, widthMeasureSpec, 0),
heightSizeAndState);
}
@Override
protected void onLayout(boolean changed, int l, int t, int r, int b) {
layoutChild();
}
private void layoutChild() {
int childTop = getPaddingTop();
int childLeft = getPaddingLeft();
for (int i = 0; i < getChildCount(); i++) {
View child = getChildAt(i);
final int childWidth = child.getMeasuredWidth();
int childHeight = child.getMeasuredHeight();
final LayoutParams lp =
(LayoutParams) child.getLayoutParams();
childTop += lp.topMargin;
if (i != 0) {
childTop -= mOverlapGaps * 2;
child.layout(childLeft, childTop, childLeft + childWidth, childTop + childHeight);
} else {
child.layout(childLeft, childTop, childLeft + childWidth, childTop + childHeight);
}
childTop += lp.mHeaderHeight;
}
}
public void updateSelectPosition(final int selectPosition) {
post(new Runnable() {
@Override
public void run() {
doCardClickAnimation(mViewHolders.get(selectPosition), selectPosition);
}
});
}
public void clearSelectPosition() {
updateSelectPosition(mSelectPosition);
}
public void clearScrollYAndTranslation() {
if (mSelectPosition != DEFAULT_SELECT_POSITION) {
clearSelectPosition();
}
if (mScrollDelegate != null) mScrollDelegate.setViewScrollY(0);
requestLayout();
}
public void setAdapter(StackAdapter stackAdapter) {
mStackAdapter = stackAdapter;
mStackAdapter.registerObserver(mObserver);
refreshView();
}
public void setAnimationType(int type) {
AnimatorAdapter animatorAdapter;
switch (type) {
case ALL_DOWN:
animatorAdapter = new AllMoveDownAnimatorAdapter(this);
break;
case UP_DOWN:
animatorAdapter = new UpDownAnimatorAdapter(this);
break;
default:
animatorAdapter = new UpDownStackAnimatorAdapter(this);
break;
}
setAnimatorAdapter(animatorAdapter);
}
public void setAnimatorAdapter(AnimatorAdapter animatorAdapter) {
clearScrollYAndTranslation();
mAnimatorAdapter = animatorAdapter;
if (mAnimatorAdapter instanceof UpDownStackAnimatorAdapter) {
mScrollDelegate = new StackScrollDelegateImpl(this);
} else {
mScrollDelegate = this;
}
}
private void refreshView() {
removeAllViews();
mViewHolders.clear();
for (int i = 0; i < mStackAdapter.getItemCount(); i++) {
ViewHolder holder = getViewHolder(i);
holder.position = i;
holder.onItemExpand(i == mSelectPosition);
addView(holder.itemView);
setClickAnimator(holder, i);
mStackAdapter.bindViewHolder(holder, i);
}
requestLayout();
}
ViewHolder getViewHolder(int i) {
if (i == DEFAULT_SELECT_POSITION) return null;
ViewHolder viewHolder;
if (mViewHolders.size() <= i || mViewHolders.get(i).mItemViewType != mStackAdapter.getItemViewType(i)) {
viewHolder = mStackAdapter.createView(this, mStackAdapter.getItemViewType(i));
mViewHolders.add(viewHolder);
} else {
viewHolder = mViewHolders.get(i);
}
return viewHolder;
}
private void setClickAnimator(final ViewHolder holder, final int position) {
setOnClickListener(new OnClickListener() {
@Override
public void onClick(View v) {
if (mSelectPosition == DEFAULT_SELECT_POSITION) return;
performItemClick(mViewHolders.get(mSelectPosition));
}
});
holder.itemView.setOnClickListener(new OnClickListener() {
@Override
public void onClick(View v) {
performItemClick(holder);
}
});
}
public void next() {
if (mSelectPosition == DEFAULT_SELECT_POSITION || mSelectPosition == mViewHolders.size() - 1) return;
performItemClick(mViewHolders.get(mSelectPosition + 1));
}
public void pre() {
if (mSelectPosition == DEFAULT_SELECT_POSITION || mSelectPosition == 0) return;
performItemClick(mViewHolders.get(mSelectPosition - 1));
}
public boolean isExpending() {
return mSelectPosition != DEFAULT_SELECT_POSITION;
}
public void performItemClick(ViewHolder viewHolder) {
doCardClickAnimation(viewHolder, viewHolder.position);
}
private void doCardClickAnimation(final ViewHolder viewHolder, int position) {
checkContentHeightByParent();
mAnimatorAdapter.itemClick(viewHolder, position);
}
private void initOrResetVelocityTracker() {
if (mVelocityTracker == null) {
mVelocityTracker = VelocityTracker.obtain();
} else {
mVelocityTracker.clear();
}
}
private void initVelocityTrackerIfNotExists() {
if (mVelocityTracker == null) {
mVelocityTracker = VelocityTracker.obtain();
}
}
private void recycleVelocityTracker() {
if (mVelocityTracker != null) {
mVelocityTracker.recycle();
mVelocityTracker = null;
}
}
@Override
public boolean onInterceptTouchEvent(MotionEvent ev) {
final int action = ev.getAction();
if ((action == MotionEvent.ACTION_MOVE) && (mIsBeingDragged)) {
return true;
}
if (getViewScrollY() == 0 && !canScrollVertically(1)) {
return false;
}
switch (action & MotionEvent.ACTION_MASK) {
case MotionEvent.ACTION_MOVE: {
final int activePointerId = mActivePointerId;
if (activePointerId == INVALID_POINTER) {
break;
}
final int pointerIndex = ev.findPointerIndex(activePointerId);
if (pointerIndex == -1) {
Log.e(TAG, "Invalid pointerId=" + activePointerId
+ " in onInterceptTouchEvent");
break;
}
final int y = (int) ev.getY(pointerIndex);
final int yDiff = Math.abs(y - mLastMotionY);
if (yDiff > mTouchSlop) {
mIsBeingDragged = true;
mLastMotionY = y;
initVelocityTrackerIfNotExists();
mVelocityTracker.addMovement(ev);
mNestedYOffset = 0;
final ViewParent parent = getParent();
if (parent != null) {
parent.requestDisallowInterceptTouchEvent(true);
}
}
break;
}
case MotionEvent.ACTION_DOWN: {
final int y = (int) ev.getY();
mLastMotionY = y;
mActivePointerId = ev.getPointerId(0);
initOrResetVelocityTracker();
mVelocityTracker.addMovement(ev);
mIsBeingDragged = !mScroller.isFinished();
break;
}
case MotionEvent.ACTION_CANCEL:
case MotionEvent.ACTION_UP:
mIsBeingDragged = false;
mActivePointerId = INVALID_POINTER;
recycleVelocityTracker();
if (mScroller.springBack(getViewScrollX(), getViewScrollY(), 0, 0, 0, getScrollRange())) {
postInvalidate();
}
break;
case MotionEvent.ACTION_POINTER_UP:
onSecondaryPointerUp(ev);
break;
}
if (!mScrollEnable) {
mIsBeingDragged = false;
}
return mIsBeingDragged;
}
@Override
public boolean onTouchEvent(MotionEvent ev) {
if (!mIsBeingDragged) {
super.onTouchEvent(ev);
}
if (!mScrollEnable) {
return true;
}
initVelocityTrackerIfNotExists();
MotionEvent vtev = MotionEvent.obtain(ev);
final int actionMasked = ev.getActionMasked();
if (actionMasked == MotionEvent.ACTION_DOWN) {
mNestedYOffset = 0;
}
vtev.offsetLocation(0, mNestedYOffset);
switch (actionMasked) {
case MotionEvent.ACTION_DOWN: {
if (getChildCount() == 0) {
return false;
}
if ((mIsBeingDragged = !mScroller.isFinished())) {
final ViewParent parent = getParent();
if (parent != null) {
parent.requestDisallowInterceptTouchEvent(true);
}
}
if (!mScroller.isFinished()) {
mScroller.abortAnimation();
}
mLastMotionY = (int) ev.getY();
mActivePointerId = ev.getPointerId(0);
break;
}
case MotionEvent.ACTION_MOVE:
final int activePointerIndex = ev.findPointerIndex(mActivePointerId);
if (activePointerIndex == -1) {
Log.e(TAG, "Invalid pointerId=" + mActivePointerId + " in onTouchEvent");
break;
}
final int y = (int) ev.getY(activePointerIndex);
int deltaY = mLastMotionY - y;
if (!mIsBeingDragged && Math.abs(deltaY) > mTouchSlop) {
final ViewParent parent = getParent();
if (parent != null) {
parent.requestDisallowInterceptTouchEvent(true);
}
mIsBeingDragged = true;
if (deltaY > 0) {
deltaY -= mTouchSlop;
} else {
deltaY += mTouchSlop;
}
}
if (mIsBeingDragged) {
mLastMotionY = y - mScrollOffset[1];
final int range = getScrollRange();
if (mScrollDelegate instanceof StackScrollDelegateImpl) {
mScrollDelegate.scrollViewTo(0, deltaY + mScrollDelegate.getViewScrollY());
} else {
if (overScrollBy(0, deltaY, 0, getViewScrollY(),
0, range, 0, 0, true)) {
mVelocityTracker.clear();
}
}
}
break;
case MotionEvent.ACTION_UP:
if (mIsBeingDragged) {
final VelocityTracker velocityTracker = mVelocityTracker;
velocityTracker.computeCurrentVelocity(1000, mMaximumVelocity);
int initialVelocity = (int) velocityTracker.getYVelocity(mActivePointerId);
if (getChildCount() > 0) {
if ((Math.abs(initialVelocity) > mMinimumVelocity)) {
fling(-initialVelocity);
} else {
if (mScroller.springBack(getViewScrollX(), mScrollDelegate.getViewScrollY(), 0, 0, 0,
getScrollRange())) {
postInvalidate();
}
}
mActivePointerId = INVALID_POINTER;
}
}
endDrag();
break;
case MotionEvent.ACTION_CANCEL:
if (mIsBeingDragged && getChildCount() > 0) {
if (mScroller.springBack(getViewScrollX(), mScrollDelegate.getViewScrollY(), 0, 0, 0, getScrollRange())) {
postInvalidate();
}
mActivePointerId = INVALID_POINTER;
}
endDrag();
break;
case MotionEvent.ACTION_POINTER_DOWN: {
final int index = ev.getActionIndex();
mLastMotionY = (int) ev.getY(index);
mActivePointerId = ev.getPointerId(index);
break;
}
case MotionEvent.ACTION_POINTER_UP:
onSecondaryPointerUp(ev);
mLastMotionY = (int) ev.getY(ev.findPointerIndex(mActivePointerId));
break;
}
if (mVelocityTracker != null) {
mVelocityTracker.addMovement(vtev);
}
vtev.recycle();
return true;
}
private void onSecondaryPointerUp(MotionEvent ev) {
final int pointerIndex = (ev.getAction() & MotionEvent.ACTION_POINTER_INDEX_MASK) >>
MotionEvent.ACTION_POINTER_INDEX_SHIFT;
final int pointerId = ev.getPointerId(pointerIndex);
if (pointerId == mActivePointerId) {
final int newPointerIndex = pointerIndex == 0 ? 1 : 0;
mLastMotionY = (int) ev.getY(newPointerIndex);
mActivePointerId = ev.getPointerId(newPointerIndex);
if (mVelocityTracker != null) {
mVelocityTracker.clear();
}
}
}
private int getScrollRange() {
int scrollRange = 0;
if (getChildCount() > 0) {
scrollRange = Math.max(0,
mTotalLength - mShowHeight);
}
return scrollRange;
}
@Override
protected int computeVerticalScrollRange() {
final int count = getChildCount();
final int contentHeight = mShowHeight;
if (count == 0) {
return contentHeight;
}
int scrollRange = mTotalLength;
final int scrollY = mScrollDelegate.getViewScrollY();
final int overscrollBottom = Math.max(0, scrollRange - contentHeight);
if (scrollY < 0) {
scrollRange -= scrollY;
} else if (scrollY > overscrollBottom) {
scrollRange += scrollY - overscrollBottom;
}
return scrollRange;
}
@Override
protected void onOverScrolled(int scrollX, int scrollY,
boolean clampedX, boolean clampedY) {
if (!mScroller.isFinished()) {
final int oldX = mScrollDelegate.getViewScrollX();
final int oldY = mScrollDelegate.getViewScrollY();
mScrollDelegate.setViewScrollX(scrollX);
mScrollDelegate.setViewScrollY(scrollY);
onScrollChanged(mScrollDelegate.getViewScrollX(), mScrollDelegate.getViewScrollY(), oldX, oldY);
if (clampedY) {
mScroller.springBack(mScrollDelegate.getViewScrollX(), mScrollDelegate.getViewScrollY(), 0, 0, 0, getScrollRange());
}
} else {
super.scrollTo(scrollX, scrollY);
}
}
@Override
protected int computeVerticalScrollOffset() {
return Math.max(0, super.computeVerticalScrollOffset());
}
@Override
public void computeScroll() {
if (mScroller.computeScrollOffset()) {
mScrollDelegate.scrollViewTo(0, mScroller.getCurrY());
postInvalidate();
}
}
public void fling(int velocityY) {
if (getChildCount() > 0) {
int height = mShowHeight;
int bottom = mTotalLength;
mScroller.fling(mScrollDelegate.getViewScrollX(), mScrollDelegate.getViewScrollY(), 0, velocityY, 0, 0, 0,
Math.max(0, bottom - height), 0, 0);
postInvalidate();
}
}
@Override
public void scrollTo(int x, int y) {
if (getChildCount() > 0) {
x = clamp(x, getWidth() - getPaddingRight() - getPaddingLeft(), getWidth());
y = clamp(y, mShowHeight, mTotalLength);
if (x != mScrollDelegate.getViewScrollX() || y != mScrollDelegate.getViewScrollY()) {
super.scrollTo(x, y);
}
}
}
@Override
public int getViewScrollX() {
return getScrollX();
}
@Override
public void scrollViewTo(int x, int y) {
scrollTo(x, y);
}
@Override
public void setViewScrollY(int y) {
setScrollY(y);
}
@Override
public void setViewScrollX(int x) {
setScrollX(x);
}
@Override
public int getViewScrollY() {
return getScrollY();
}
private void endDrag() {
mIsBeingDragged = false;
recycleVelocityTracker();
}
private static int clamp(int n, int my, int child) {
if (my >= child || n < 0) {
return 0;
}
if ((my + n) > child) {
return child - my;
}
return n;
}
@Override
public ViewGroup.LayoutParams generateLayoutParams(AttributeSet attrs) {
return new LayoutParams(getContext(), attrs);
}
@Override
protected ViewGroup.LayoutParams generateDefaultLayoutParams() {
return new LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT);
}
@Override
protected ViewGroup.LayoutParams generateLayoutParams(ViewGroup.LayoutParams p) {
return new LayoutParams(p);
}
@Override
protected boolean checkLayoutParams(ViewGroup.LayoutParams p) {
return p instanceof LayoutParams;
}
public static class LayoutParams extends MarginLayoutParams {
public int mHeaderHeight;
public LayoutParams(Context c, AttributeSet attrs) {
super(c, attrs);
TypedArray array = c.obtainStyledAttributes(attrs, R.styleable.CardStackView);
mHeaderHeight = array.getDimensionPixelSize(R.styleable.CardStackView_stackHeaderHeight, -1);
}
public LayoutParams(int width, int height) {
super(width, height);
}
public LayoutParams(ViewGroup.LayoutParams source) {
super(source);
}
}
public static abstract class Adapter<VH extends ViewHolder> {
private final AdapterDataObservable mObservable = new AdapterDataObservable();
VH createView(ViewGroup parent, int viewType) {
VH holder = onCreateView(parent, viewType);
holder.mItemViewType = viewType;
return holder;
}
protected abstract VH onCreateView(ViewGroup parent, int viewType);
public void bindViewHolder(VH holder, int position) {
onBindViewHolder(holder, position);
}
protected abstract void onBindViewHolder(VH holder, int position);
public abstract int getItemCount();
public int getItemViewType(int position) {
return 0;
}
public final void notifyDataSetChanged() {
mObservable.notifyChanged();
}
public void registerObserver(AdapterDataObserver observer) {
mObservable.registerObserver(observer);
}
}
public static abstract class ViewHolder {
public View itemView;
int mItemViewType = INVALID_TYPE;
int position;
public ViewHolder(View view) {
itemView = view;
}
public Context getContext() {
return itemView.getContext();
}
public abstract void onItemExpand(boolean b);
protected void onAnimationStateChange(int state, boolean willBeSelect) {
}
}
public static class AdapterDataObservable extends Observable<AdapterDataObserver> {
public boolean hasObservers() {
return !mObservers.isEmpty();
}
public void notifyChanged() {
for (int i = mObservers.size() - 1; i >= 0; i--) {
mObservers.get(i).onChanged();
}
}
}
public static abstract class AdapterDataObserver {
public void onChanged() {
}
}
private class ViewDataObserver extends AdapterDataObserver {
@Override
public void onChanged() {
refreshView();
}
}
public int getSelectPosition() {
return mSelectPosition;
}
public void setSelectPosition(int selectPosition) {
mSelectPosition = selectPosition;
mItemExpendListener.onItemExpend(mSelectPosition != DEFAULT_SELECT_POSITION);
}
public int getOverlapGaps() {
return mOverlapGaps;
}
public void setOverlapGaps(int overlapGaps) {
mOverlapGaps = overlapGaps;
}
public int getOverlapGapsCollapse() {
return mOverlapGapsCollapse;
}
public void setOverlapGapsCollapse(int overlapGapsCollapse) {
mOverlapGapsCollapse = overlapGapsCollapse;
}
public void setScrollEnable(boolean scrollEnable) {
mScrollEnable = scrollEnable;
}
public int getShowHeight() {
return mShowHeight;
}
public int getTotalLength() {
return mTotalLength;
}
public void setDuration(int duration) {
mDuration = duration;
}
public int getDuration() {
if (mAnimatorAdapter != null) return mDuration;
return 0;
}
public void setNumBottomShow(int numBottomShow) {
mNumBottomShow = numBottomShow;
}
public int getNumBottomShow() {
return mNumBottomShow;
}
public ScrollDelegate getScrollDelegate() {
return mScrollDelegate;
}
public ItemExpendListener getItemExpendListener() {
return mItemExpendListener;
}
public void setItemExpendListener(ItemExpendListener itemExpendListener) {
mItemExpendListener = itemExpendListener;
}
public interface ItemExpendListener{
void onItemExpend(boolean expend);
}
}