package com.huxq17.swipecardsview; import android.annotation.TargetApi; import android.content.Context; import android.content.res.TypedArray; import android.graphics.Rect; import android.os.Build; import android.support.v4.view.ViewCompat; import android.util.AttributeSet; import android.view.Gravity; import android.view.LayoutInflater; import android.view.MotionEvent; import android.view.VelocityTracker; import android.view.View; import android.view.ViewConfiguration; import android.view.animation.Interpolator; import android.widget.LinearLayout; import android.widget.Scroller; import java.util.ArrayList; import java.util.List; public class SwipeCardsView extends LinearLayout { private List<View> viewList = new ArrayList<>(); // 存放的是每一层的view,从顶到底 private List<View> releasedViewList = new ArrayList<>(); // 手指松开后存放的view列表 private int initLeft = 0, initTop = 0; // 正常状态下 topView的left和top private int mWidth = 0; // swipeCardsView的宽度 private int mHeight = 0; // swipeCardsView的高度 private int mCardWidth = 0; // 每一个子View对应的宽度 private static final int MAX_SLIDE_DISTANCE_LINKAGE = 400; // 水平距离+垂直距离 private int yOffsetStep = 0; // view叠加垂直偏移量的步长 private float scaleOffsetStep = 0f; // view叠加缩放的步长 private int alphaOffsetStep = 0; //view叠加透明度的步长 private static final int X_VEL_THRESHOLD = 900; private static final int X_DISTANCE_THRESHOLD = 300; private CardsSlideListener mCardsSlideListener; // 回调接口 private int mCount; // 卡片的数量 private int mShowingIndex = 0; // 当前正在显示的卡片位置 private OnClickListener btnListener; private BaseCardAdapter mAdapter; private Scroller mScroller; private int mTouchSlop; private int mLastY = -1; // save event y private int mLastX = -1; // save event x private int mInitialMotionY; private int mInitialMotionX; private final int SCROLL_DURATION = 300; // scroll back duration private boolean hasTouchTopView; private VelocityTracker mVelocityTracker; private float mMaxVelocity; private float mMinVelocity; private boolean isIntercepted = false; private boolean isTouching = false; private int tempShowingIndex = -1; private int cardVisibleCount = 3; /** * 卡片是否在移动的标记,如果在移动中则不执行onLayout中的layout操作 */ private boolean mScrolling = false; public SwipeCardsView(Context context) { this(context, null); } public SwipeCardsView(Context context, AttributeSet attrs) { this(context, attrs, 0); } public SwipeCardsView(Context context, AttributeSet attrs, int defStyle) { super(context, attrs, defStyle); TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.SwipCardsView); yOffsetStep = (int) a.getDimension(R.styleable.SwipCardsView_yOffsetStep, yOffsetStep); alphaOffsetStep = a.getInt(R.styleable.SwipCardsView_alphaOffsetStep, alphaOffsetStep); scaleOffsetStep = a.getFloat(R.styleable.SwipCardsView_scaleOffsetStep, scaleOffsetStep); a.recycle(); btnListener = new OnClickListener() { @Override public void onClick(View view) { if (null != mCardsSlideListener && view.getScaleX() == 1f) { mCardsSlideListener.onItemClick(view, mShowingIndex); } } }; mScroller = new Scroller(getContext(), sInterpolator); mTouchSlop = ViewConfiguration.get(getContext()).getScaledTouchSlop(); mMaxVelocity = ViewConfiguration.get(getContext()).getScaledMaximumFlingVelocity(); mMinVelocity = ViewConfiguration.get(getContext()).getScaledMinimumFlingVelocity(); // setWillNotDraw(false); } /** * Interpolator defining the animation curve for mScroller */ private static final Interpolator sInterpolator = new Interpolator() { private float mTension = 1.6f; @Override public float getInterpolation(float t) { t -= 1.0f; return t * t * ((mTension + 1) * t + mTension) + 1.0f; } }; private int getCardLayoutId(int layoutid) { String resourceTypeName = getContext().getResources().getResourceTypeName(layoutid); if (!resourceTypeName.contains("layout")) { String errorMsg = getContext().getResources().getResourceName(layoutid) + " is a illegal layoutid , please check your layout id first !"; throw new RuntimeException(errorMsg); } return layoutid; } private void bindCardData(int position, View cardview) { if (mAdapter != null) { mAdapter.onBindData(position, cardview); cardview.setTag(position); } cardview.setVisibility(View.VISIBLE); } private boolean mWaitRefresh = false; /** * 刷新ui * * @param index 当前显示的卡片下标 */ public void notifyDatasetChanged(int index) { if (canResetView()) { // LogUtil.d("test notifyDatasetChanged canResetView=" + index); refreshUI(index); } else { // LogUtil.d("test notifyDatasetChanged can not Reset View=" + index); mWaitRefresh = true; tempShowingIndex = index; } } private void refreshUI(int index) { if (mAdapter == null) { throw new RuntimeException("adapter==null"); } mShowingIndex = index; mCount = mAdapter.getCount(); // cardVisibleCount = mAdapter.getVisibleCardCount(); cardVisibleCount = Math.min(cardVisibleCount, mCount); for (int i = mShowingIndex; i < mShowingIndex + cardVisibleCount; i++) { View childView = viewList.get(i - mShowingIndex); if (childView == null) { return; } if (i < mCount) { bindCardData(i, childView); } else { childView.setVisibility(View.GONE); } setOnItemClickListener(childView); } if (null != mCardsSlideListener) { mCardsSlideListener.onShow(mShowingIndex); } } private void setOnItemClickListener(View childView) { childView.setOnClickListener(btnListener); } public void setAdapter(BaseCardAdapter adapter) { if (adapter == null) { throw new RuntimeException("adapter==null"); } mAdapter = adapter; mShowingIndex = 0; removeAllViewsInLayout(); viewList.clear(); mCount = mAdapter.getCount(); int cardVisibleCount = mAdapter.getVisibleCardCount(); cardVisibleCount = Math.min(cardVisibleCount, mCount); for (int i = mShowingIndex; i < mShowingIndex + cardVisibleCount; i++) { View childView = LayoutInflater.from(getContext()).inflate(getCardLayoutId(mAdapter.getCardLayoutId()), this, false); if (childView == null) { return; } if (i < mCount) { bindCardData(i, childView); } else { childView.setVisibility(View.GONE); } viewList.add(childView); setOnItemClickListener(childView); addView(childView, 0); } if (null != mCardsSlideListener) { mCardsSlideListener.onShow(mShowingIndex); } } private boolean mRetainLastCard = false; /** * whether retain last card * * @param retain defalut false */ public void retainLastCard(boolean retain) { mRetainLastCard = retain; } private boolean canMoveCard() { return !mRetainLastCard || mRetainLastCard && mShowingIndex != mCount - 1; } private boolean mEnableSwipe = true; public void enableSwipe(boolean enable) { mEnableSwipe = enable; } @Override public boolean dispatchTouchEvent(MotionEvent ev) { final int action = ev.getActionMasked(); acquireVelocityTracker(ev); int deltaY = 0; int deltaX = 0; switch (action) { case MotionEvent.ACTION_DOWN: mScroller.abortAnimation(); resetViewGroup(); if (isTouchTopView(ev) && canMoveCard() && mEnableSwipe) { isTouching = true; } hasTouchTopView = false; mLastY = (int) ev.getRawY(); mLastX = (int) ev.getRawX(); mInitialMotionY = mLastY; mInitialMotionX = mLastX; break; case MotionEvent.ACTION_MOVE: if (!canMoveCard() || !mEnableSwipe) { return super.dispatchTouchEvent(ev); } mLastMoveEvent = ev; int currentY = (int) ev.getRawY(); int currentX = (int) ev.getRawX(); deltaY = currentY - mLastY; deltaX = currentX - mLastX; mLastY = currentY; mLastX = currentX; if (!isIntercepted) { int distanceX = Math.abs(currentX - mInitialMotionX); int distanceY = Math.abs(currentY - mInitialMotionY); if (distanceX * distanceX + distanceY + distanceY >= mTouchSlop * mTouchSlop) { isIntercepted = true; } else { return super.dispatchTouchEvent(ev); } } if (isIntercepted && (hasTouchTopView || isTouchTopView(ev))) { hasTouchTopView = true; moveTopView(deltaX, deltaY); invalidate(); sendCancelEvent(); return true; } break; case MotionEvent.ACTION_UP: case MotionEvent.ACTION_CANCEL: hasTouchTopView = false; isTouching = false; isIntercepted = false; mHasSendCancelEvent = false; mVelocityTracker.computeCurrentVelocity(1000, mMaxVelocity); final float velocityX = mVelocityTracker.getXVelocity(); final float velocityY = mVelocityTracker.getYVelocity(); final float xvel = clampMag(velocityX, mMinVelocity, mMaxVelocity); final float yvel = clampMag(velocityY, mMinVelocity, mMaxVelocity); releaseTopView(xvel, yvel); releaseVelocityTracker(); // invalidate(); break; } return super.dispatchTouchEvent(ev); } private MotionEvent mLastMoveEvent; private boolean mHasSendCancelEvent = false; private void sendCancelEvent() { if (!mHasSendCancelEvent) { mHasSendCancelEvent = true; MotionEvent last = mLastMoveEvent; MotionEvent e = MotionEvent.obtain( last.getDownTime(), last.getEventTime() + ViewConfiguration.getLongPressTimeout(), MotionEvent.ACTION_CANCEL, last.getX(), last.getY(), last.getMetaState()); dispatchTouchEventSupper(e); } } // @Override // protected boolean drawChild(Canvas canvas, View child, long drawingTime) { // boolean ret = false; // int canvasRestore = -1; // int index = indexOfChild(child); // View topView = getTopView(); // LogUtil.e("index=" + index + " is topview " + (topView == child)); // if (topView != child) { // Rect thisRect = new Rect(); // getGlobalVisibleRect(thisRect); // Rect childRect = new Rect(); // child.getGlobalVisibleRect(childRect); // childRect.set(childRect.left - thisRect.left, childRect.top - thisRect.top, childRect.left - thisRect.left + childRect.width(), childRect.height() + childRect.top - thisRect.top); // Rect topRect = new Rect(); // topView.getGlobalVisibleRect(topRect); // topRect.set(topRect.left - thisRect.left, topRect.top - thisRect.top, topRect.left - thisRect.left + topRect.width(), topRect.height() + topRect.top - thisRect.top); // Rect newrect = new Rect(thisRect.left, topRect.bottom, thisRect.right, child.getBottom()); // canvasRestore = canvas.save(); // canvas.clipRect(topRect, Region.Op.XOR); // //// Paint mPaint = new Paint(); //// mPaint.setAntiAlias(true); //// mPaint.setStyle(Paint.Style.STROKE); //// mPaint.setColor(Color.RED); //// canvas.drawRect(newrect, mPaint); // LogUtil.e("test index=" + index + ";newrect=" + newrect.toShortString() + "child.bottom=" + child.getBottom() + ";thisRect=" + thisRect.toShortString() + ";canvas.getClipBounds()=" + canvas.getClipBounds().toShortString()); // } // ret = super.drawChild(canvas, child, drawingTime); // if (canvasRestore != -1) { // canvas.restoreToCount(canvasRestore); // } // return ret; // } public boolean dispatchTouchEventSupper(MotionEvent e) { return super.dispatchTouchEvent(e); } private void releaseTopView(float xvel, float yvel) { mScrolling = true; View topView = getTopView(); if (topView != null && canMoveCard() && mEnableSwipe) { onTopViewReleased(topView, xvel, yvel); } } /** * 是否摸到了某个view * * @param ev * @return */ private boolean isTouchTopView(MotionEvent ev) { View topView = getTopView(); if (topView != null && topView.getVisibility() == VISIBLE) { Rect bounds = new Rect(); topView.getGlobalVisibleRect(bounds); int x = (int) ev.getRawX(); int y = (int) ev.getRawY(); if (bounds.contains(x, y)) { return true; } else { return false; } } return false; } private void moveTopView(int deltaX, int deltaY) { View topView = getTopView(); if (topView != null) { topView.offsetLeftAndRight(deltaX); topView.offsetTopAndBottom(deltaY); processLinkageView(topView); } } private View getTopView() { if (viewList.size() > 0) { return viewList.get(0); } return null; } public void startScrollTopView(int finalLeft, int finalTop, int duration, SlideType flyType) { View topView = getTopView(); if (topView == null) { mScrolling = false; return; } if (finalLeft != initLeft) { releasedViewList.add(topView); } final int startLeft = topView.getLeft(); final int startTop = topView.getTop(); final int dx = finalLeft - startLeft; final int dy = finalTop - startTop; if (dx != 0 || dy != 0) { mScroller.startScroll(topView.getLeft(), topView.getTop(), dx, dy, duration); ViewCompat.postInvalidateOnAnimation(this); } else { mScrolling = false; } if (flyType != SlideType.NONE && mCardsSlideListener != null) { mCardsSlideListener.onCardVanish(mShowingIndex, flyType); } } /** * @param event 向VelocityTracker添加MotionEvent * @see android.view.VelocityTracker#obtain() * @see android.view.VelocityTracker#addMovement(MotionEvent) */ private void acquireVelocityTracker(final MotionEvent event) { if (null == mVelocityTracker) { mVelocityTracker = VelocityTracker.obtain(); } mVelocityTracker.addMovement(event); } /** * 释放VelocityTracker * * @see android.view.VelocityTracker#clear() * @see android.view.VelocityTracker#recycle() */ private void releaseVelocityTracker() { if (null != mVelocityTracker) { mVelocityTracker.clear(); mVelocityTracker.recycle(); mVelocityTracker = null; } } /** * Clamp the magnitude of value for absMin and absMax. * If the value is below the minimum, it will be clamped to zero. * If the value is above the maximum, it will be clamped to the maximum. * * @param value Value to clamp * @param absMin Absolute value of the minimum significant value to return * @param absMax Absolute value of the maximum value to return * @return The clamped value with the same sign as <code>value</code> */ private float clampMag(float value, float absMin, float absMax) { final float absValue = Math.abs(value); if (absValue < absMin) return 0; if (absValue > absMax) return value > 0 ? absMax : -absMax; return value; } @Override protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { measureChildrenWithMargins(widthMeasureSpec, heightMeasureSpec); int maxWidth = MeasureSpec.getSize(widthMeasureSpec); int maxHeight = MeasureSpec.getSize(heightMeasureSpec); setMeasuredDimension(resolveSizeAndState(maxWidth, widthMeasureSpec, 0), resolveSizeAndState(maxHeight, heightMeasureSpec, 0)); mWidth = getMeasuredWidth(); mHeight = getMeasuredHeight(); } private void measureChildrenWithMargins(int widthMeasureSpec, int heightMeasureSpec) { final int size = getChildCount(); for (int i = 0; i < size; ++i) { final View child = getChildAt(i); if (child.getVisibility() != GONE) { measureChildWithMargins(child, widthMeasureSpec, 0, heightMeasureSpec, 0); } } } @Override protected void onLayout(boolean changed, int left, int top, int right, int bottom) { if (hasTouchTopView || mScrolling) { return; } int size = viewList.size(); if (size == 0) { return; } for (int i = 0; i < size; i++) { View child = viewList.get(i); layoutChild(child, i); } // 初始化一些中间参数 initLeft = viewList.get(0).getLeft(); initTop = viewList.get(0).getTop(); mCardWidth = viewList.get(0).getMeasuredWidth(); } @TargetApi(Build.VERSION_CODES.JELLY_BEAN_MR1) private void layoutChild(View child, int index) { LayoutParams lp = (LayoutParams) child.getLayoutParams(); int width = child.getMeasuredWidth(); int height = child.getMeasuredHeight(); int gravity = lp.gravity; if (gravity == -1) { gravity = Gravity.TOP | Gravity.START; } int layoutDirection = getLayoutDirection(); final int absoluteGravity = Gravity.getAbsoluteGravity(gravity, layoutDirection); final int verticalGravity = gravity & Gravity.VERTICAL_GRAVITY_MASK; int childLeft; int childTop; switch (absoluteGravity & Gravity.HORIZONTAL_GRAVITY_MASK) { case Gravity.CENTER_HORIZONTAL: childLeft = (getWidth() + getPaddingLeft() - getPaddingRight() - width) / 2 + lp.leftMargin - lp.rightMargin; break; case Gravity.END: childLeft = getWidth() + getPaddingRight() - width - lp.rightMargin; break; case Gravity.START: default: childLeft = getPaddingLeft() + lp.leftMargin; break; } switch (verticalGravity) { case Gravity.CENTER_VERTICAL: childTop = (getHeight() + getPaddingTop() - getPaddingBottom() - height) / 2 + lp.topMargin - lp.bottomMargin; break; case Gravity.BOTTOM: childTop = getHeight() - getPaddingBottom() - height - lp.bottomMargin; break; case Gravity.TOP: default: childTop = getPaddingTop() + lp.topMargin; break; } child.layout(childLeft, childTop, childLeft + width, childTop + height); int offset = yOffsetStep * index; float scale = 1 - scaleOffsetStep * index; float alpha = 1.0f * (100 - alphaOffsetStep * index) / 100; child.offsetTopAndBottom(offset); child.setScaleX(scale); child.setScaleY(scale); child.setAlpha(alpha); } @Override public void computeScroll() { if (mScroller.computeScrollOffset()) { View topView = getTopView(); if (topView == null) { return; } final int x = mScroller.getCurrX(); final int y = mScroller.getCurrY(); final int dx = x - topView.getLeft(); final int dy = y - topView.getTop(); if (x != mScroller.getFinalX() || y != mScroller.getFinalY()) { moveTopView(dx, dy); } ViewCompat.postInvalidateOnAnimation(this); } else { mScrolling = false; onAnimalStop(); } } private void onAnimalStop() { if (canResetView()) { resetViewGroup(); } } private boolean canResetView() { return !mScroller.computeScrollOffset() && !isTouching; } /** * 对View重新排序 */ private void resetViewGroup() { if (releasedViewList.size() == 0) { mScrolling = false; if (mWaitRefresh) { mWaitRefresh = false; refreshUI(tempShowingIndex); } if (viewList.size() != 0) { View topView = getTopView(); if (topView != null) { if (topView.getLeft() != initLeft || topView.getTop() != initTop) { topView.offsetLeftAndRight(initLeft - topView.getLeft()); topView.offsetTopAndBottom(initTop - topView.getTop()); } } } } else { View changedView = releasedViewList.get(0); if (changedView.getLeft() == initLeft) { releasedViewList.remove(0); mScrolling = false; return; } viewList.remove(changedView); viewList.add(changedView); mScrolling = false; int viewSize = viewList.size(); removeViewInLayout(changedView); addViewInLayout(changedView, 0, changedView.getLayoutParams(), true); requestLayout(); // removeView(changedView); // addView(changedView,0); if (mWaitRefresh) { mWaitRefresh = false; int index = ++tempShowingIndex; refreshUI(index); } else { int newIndex = mShowingIndex + viewSize; if (newIndex < mCount) { bindCardData(newIndex, changedView); } else { changedView.setVisibility(View.GONE); } if (mShowingIndex + 1 < mCount) { mShowingIndex++; if (null != mCardsSlideListener) { mCardsSlideListener.onShow(mShowingIndex); } } else { //no card showing mShowingIndex = -1; } } releasedViewList.remove(0); } tempShowingIndex = -1; } /** * 顶层卡片View位置改变,底层的位置需要调整 * * @param changedView 顶层的卡片view */ private void processLinkageView(View changedView) { int changeViewLeft = changedView.getLeft(); int changeViewTop = changedView.getTop(); int distance = Math.abs(changeViewTop - initTop) + Math.abs(changeViewLeft - initLeft); float rate = distance / (float) MAX_SLIDE_DISTANCE_LINKAGE; for (int i = 1; i < viewList.size(); i++) { float rate3 = rate - 0.2f * i; if (rate3 > 1) { rate3 = 1; } else if (rate3 < 0) { rate3 = 0; } ajustLinkageViewItem(changedView, rate3, i); } } // 由index对应view变成index-1对应的view private void ajustLinkageViewItem(View changedView, float rate, int index) { int changeIndex = viewList.indexOf(changedView); int initPosY = yOffsetStep * index; float initScale = 1 - scaleOffsetStep * index; float initAlpha = 1.0f * (100 - alphaOffsetStep * index) / 100; int nextPosY = yOffsetStep * (index - 1); float nextScale = 1 - scaleOffsetStep * (index - 1); float nextAlpha = 1.0f * (100 - alphaOffsetStep * (index - 1)) / 100; int offset = (int) (initPosY + (nextPosY - initPosY) * rate); float scale = initScale + (nextScale - initScale) * rate; float alpha = initAlpha + (nextAlpha - initAlpha) * rate; View ajustView = viewList.get(changeIndex + index); ajustView.offsetTopAndBottom(offset - ajustView.getTop() + initTop); ajustView.setScaleX(scale); ajustView.setScaleY(scale); ajustView.setAlpha(alpha); } /** * 松手时处理滑动到边缘的动画 * * @param xvel X方向上的滑动速度 */ private void onTopViewReleased(View changedView, float xvel, float yvel) { int finalX = initLeft; int finalY = initTop; SlideType flyType = SlideType.NONE; int dx = changedView.getLeft() - initLeft; int dy = changedView.getTop() - initTop; if (dx == 0) { // 由于dx作为分母,此处保护处理 dx = 1; } if (dx > X_DISTANCE_THRESHOLD || (xvel > X_VEL_THRESHOLD && dx > 0)) {//向右边滑出 finalX = mWidth; finalY = dy * (mCardWidth + initLeft) / dx + initTop; flyType = SlideType.RIGHT; } else if (dx < -X_DISTANCE_THRESHOLD || (xvel < -X_VEL_THRESHOLD && dx < 0)) {//向左边滑出 finalX = -mCardWidth; finalY = dy * (mCardWidth + initLeft) / (-dx) + dy + initTop; flyType = SlideType.LEFT; } if (finalY > mHeight) { finalY = mHeight; } else if (finalY < -mHeight / 2) { finalY = -mHeight / 2; } startScrollTopView(finalX, finalY, SCROLL_DURATION, flyType); } /** * use this method to Slide the card out of the screen * * @param type {@link com.huxq17.swipecardsview.SwipeCardsView.SlideType} */ public void slideCardOut(SlideType type) { if (!canMoveCard()) { return; } mScroller.abortAnimation(); resetViewGroup(); View topview = getTopView(); if (topview == null) { return; } if (releasedViewList.contains(topview) || type == SlideType.NONE) { return; } int finalX = 0; switch (type) { case LEFT: finalX = -mCardWidth; break; case RIGHT: finalX = mWidth; break; } if (finalX != 0) { startScrollTopView(finalX, initTop + mHeight, SCROLL_DURATION, type); } } public static int resolveSizeAndState(int size, int measureSpec, int childMeasuredState) { int result = size; int specMode = MeasureSpec.getMode(measureSpec); int specSize = MeasureSpec.getSize(measureSpec); switch (specMode) { case MeasureSpec.UNSPECIFIED: result = size; break; case MeasureSpec.AT_MOST: if (specSize < size) { result = specSize | MEASURED_STATE_TOO_SMALL; } else { result = size; } break; case MeasureSpec.EXACTLY: result = specSize; break; } return result | (childMeasuredState & MEASURED_STATE_MASK); } /** * 设置卡片操作回调 * * @param cardsSlideListener 回调接口 */ public void setCardsSlideListener(CardsSlideListener cardsSlideListener) { this.mCardsSlideListener = cardsSlideListener; } /** * 卡片回调接口 */ public interface CardsSlideListener { /** * 新卡片显示回调 * * @param index 最顶层显示的卡片的index */ void onShow(int index); /** * 卡片飞向两侧回调 * * @param index 飞向两侧的卡片数据index * @param type 飞向哪一侧{@link SlideType#LEFT}或{@link SlideType#RIGHT} */ void onCardVanish(int index, SlideType type); /** * 卡片点击事件 * * @param cardImageView 卡片上的图片view * @param index 点击到的index */ void onItemClick(View cardImageView, int index); } /** * <p> * {@link #LEFT} 从屏幕左边滑出 * </p> * {@link #RIGHT} 从屏幕右边滑出 */ public enum SlideType { LEFT, RIGHT, NONE } }