package com.marshalchen.common.uimodule.cardsSwiped.view; import android.animation.*; import android.content.Context; import android.content.res.TypedArray; import android.database.DataSetObserver; import android.graphics.Matrix; import android.graphics.Rect; import android.util.AttributeSet; import android.util.Log; import android.view.*; import android.view.GestureDetector.SimpleOnGestureListener; import android.view.animation.AccelerateInterpolator; import android.view.animation.LinearInterpolator; import android.widget.AdapterView; import android.widget.ListAdapter; import com.marshalchen.common.uimodule.R; import com.marshalchen.common.uimodule.cardsSwiped.model.CardModel; import com.marshalchen.common.uimodule.cardsSwiped.model.Orientations; import java.util.Random; public class CardContainer extends AdapterView<ListAdapter> { public static final int INVALID_POINTER_ID = -1; private int mActivePointerId = INVALID_POINTER_ID; private static final double DISORDERED_MAX_ROTATION_RADIANS = Math.PI / 64; private int mNumberOfCards = -1; private final DataSetObserver mDataSetObserver = new DataSetObserver() { @Override public void onChanged() { super.onChanged(); clearStack(); ensureFull(); } @Override public void onInvalidated() { super.onInvalidated(); clearStack(); } }; private final Random mRandom = new Random(); private final Rect boundsRect = new Rect(); private final Rect childRect = new Rect(); private final Matrix mMatrix = new Matrix(); //TODO: determine max dynamically based on device speed private int mMaxVisible = 10; private GestureDetector mGestureDetector; private int mFlingSlop; private Orientations.Orientation mOrientation; private ListAdapter mListAdapter; private float mLastTouchX; private float mLastTouchY; private View mTopCard; private int mTouchSlop; private int mGravity; private int mNextAdapterPosition; private boolean mDragging; public CardContainer(Context context) { super(context); setOrientation(Orientations.Orientation.Disordered); setGravity(Gravity.CENTER); init(); } public CardContainer(Context context, AttributeSet attrs) { super(context, attrs); initFromXml(attrs); init(); } public CardContainer(Context context, AttributeSet attrs, int defStyle) { super(context, attrs, defStyle); initFromXml(attrs); init(); } private void init() { ViewConfiguration viewConfiguration = ViewConfiguration.get(getContext()); mFlingSlop = viewConfiguration.getScaledMinimumFlingVelocity(); mTouchSlop = viewConfiguration.getScaledTouchSlop(); mGestureDetector = new GestureDetector(getContext(), new GestureListener()); } private void initFromXml(AttributeSet attr) { TypedArray a = getContext().obtainStyledAttributes(attr, R.styleable.CardContainer); setGravity(a.getInteger(R.styleable.CardContainer_android_gravity, Gravity.CENTER)); int orientation = a.getInteger(R.styleable.CardContainer_orientation, 1); setOrientation(Orientations.Orientation.fromIndex(orientation)); a.recycle(); } @Override public ListAdapter getAdapter() { return mListAdapter; } @Override public void setAdapter(ListAdapter adapter) { if (mListAdapter != null) mListAdapter.unregisterDataSetObserver(mDataSetObserver); clearStack(); mTopCard = null; mListAdapter = adapter; mNextAdapterPosition = 0; adapter.registerDataSetObserver(mDataSetObserver); ensureFull(); if (getChildCount() != 0) { mTopCard = getChildAt(getChildCount() - 1); mTopCard.setLayerType(LAYER_TYPE_HARDWARE, null); } mNumberOfCards = getAdapter().getCount(); requestLayout(); } private void ensureFull() { while (mNextAdapterPosition < mListAdapter.getCount() && getChildCount() < mMaxVisible) { View view = mListAdapter.getView(mNextAdapterPosition, null, this); view.setLayerType(LAYER_TYPE_SOFTWARE, null); if(mOrientation == Orientations.Orientation.Disordered) { view.setRotation(getDisorderedRotation()); } addViewInLayout(view, 0, new LayoutParams(LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT, mListAdapter.getItemViewType(mNextAdapterPosition)), false); requestLayout(); mNextAdapterPosition += 1; } } private void clearStack() { removeAllViewsInLayout(); mNextAdapterPosition = 0; mTopCard = null; } public Orientations.Orientation getOrientation() { return mOrientation; } public void setOrientation(Orientations.Orientation orientation) { if (orientation == null) throw new NullPointerException("Orientation may not be null"); if(mOrientation != orientation) { this.mOrientation = orientation; if(orientation == Orientations.Orientation.Disordered) { for (int i = 0; i < getChildCount(); i++) { View child = getChildAt(i); child.setRotation(getDisorderedRotation()); } } else { for (int i = 0; i < getChildCount(); i++) { View child = getChildAt(i); child.setRotation(0); } } requestLayout(); } } private float getDisorderedRotation() { return (float) Math.toDegrees(mRandom.nextGaussian() * DISORDERED_MAX_ROTATION_RADIANS); } @Override protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { super.onMeasure(widthMeasureSpec, heightMeasureSpec); int requestedWidth = getMeasuredWidth() - getPaddingLeft() - getPaddingRight(); int requestedHeight = getMeasuredHeight() - getPaddingTop() - getPaddingBottom(); int childWidth, childHeight; if (mOrientation == Orientations.Orientation.Disordered) { int R1, R2; if (requestedWidth >= requestedHeight) { R1 = requestedHeight; R2 = requestedWidth; } else { R1 = requestedWidth; R2 = requestedHeight; } childWidth = (int) ((R1 * Math.cos(DISORDERED_MAX_ROTATION_RADIANS) - R2 * Math.sin(DISORDERED_MAX_ROTATION_RADIANS)) / Math.cos(2 * DISORDERED_MAX_ROTATION_RADIANS)); childHeight = (int) ((R2 * Math.cos(DISORDERED_MAX_ROTATION_RADIANS) - R1 * Math.sin(DISORDERED_MAX_ROTATION_RADIANS)) / Math.cos(2 * DISORDERED_MAX_ROTATION_RADIANS)); } else { childWidth = requestedWidth; childHeight = requestedHeight; } int childWidthMeasureSpec, childHeightMeasureSpec; childWidthMeasureSpec = MeasureSpec.makeMeasureSpec(childWidth, MeasureSpec.AT_MOST); childHeightMeasureSpec = MeasureSpec.makeMeasureSpec(childHeight, MeasureSpec.AT_MOST); for (int i = 0; i < getChildCount(); i++) { View child = getChildAt(i); assert child != null; child.measure(childWidthMeasureSpec, childHeightMeasureSpec); } } @Override protected void onLayout(boolean changed, int l, int t, int r, int b) { super.onLayout(changed, l, t, r, b); for (int i = 0; i < getChildCount(); i++) { boundsRect.set(0, 0, getWidth(), getHeight()); View view = getChildAt(i); int w, h; w = view.getMeasuredWidth(); h = view.getMeasuredHeight(); Gravity.apply(mGravity, w, h, boundsRect, childRect); view.layout(childRect.left, childRect.top, childRect.right, childRect.bottom); } } @Override public boolean onTouchEvent(MotionEvent event) { if (mTopCard == null) { return false; } if (mGestureDetector.onTouchEvent(event)) { return true; } Log.d("Touch Event", MotionEvent.actionToString(event.getActionMasked()) + " "); final int pointerIndex; final float x, y; final float dx, dy; switch (event.getActionMasked()) { case MotionEvent.ACTION_DOWN: mTopCard.getHitRect(childRect); pointerIndex = event.getActionIndex(); x = event.getX(pointerIndex); y = event.getY(pointerIndex); if (!childRect.contains((int) x, (int) y)) { return false; } mLastTouchX = x; mLastTouchY = y; mActivePointerId = event.getPointerId(pointerIndex); float[] points = new float[]{x - mTopCard.getLeft(), y - mTopCard.getTop()}; mTopCard.getMatrix().invert(mMatrix); mMatrix.mapPoints(points); mTopCard.setPivotX(points[0]); mTopCard.setPivotY(points[1]); break; case MotionEvent.ACTION_MOVE: pointerIndex = event.findPointerIndex(mActivePointerId); x = event.getX(pointerIndex); y = event.getY(pointerIndex); dx = x - mLastTouchX; dy = y - mLastTouchY; if (Math.abs(dx) > mTouchSlop || Math.abs(dy) > mTouchSlop) { mDragging = true; } if(!mDragging) { return true; } mTopCard.setTranslationX(mTopCard.getTranslationX() + dx); mTopCard.setTranslationY(mTopCard.getTranslationY() + dy); mTopCard.setRotation(40 * mTopCard.getTranslationX() / (getWidth() / 2.f)); mLastTouchX = x; mLastTouchY = y; break; case MotionEvent.ACTION_UP: case MotionEvent.ACTION_CANCEL: if (!mDragging) { return true; } mDragging = false; mActivePointerId = INVALID_POINTER_ID; ValueAnimator animator = ObjectAnimator.ofPropertyValuesHolder(mTopCard, PropertyValuesHolder.ofFloat("translationX", 0), PropertyValuesHolder.ofFloat("translationY", 0), PropertyValuesHolder.ofFloat("rotation", (float) Math.toDegrees(mRandom.nextGaussian() * DISORDERED_MAX_ROTATION_RADIANS)), PropertyValuesHolder.ofFloat("pivotX", mTopCard.getWidth() / 2.f), PropertyValuesHolder.ofFloat("pivotY", mTopCard.getHeight() / 2.f) ).setDuration(250); animator.setInterpolator(new AccelerateInterpolator()); animator.start(); break; case MotionEvent.ACTION_POINTER_UP: pointerIndex = event.getActionIndex(); final int pointerId = event.getPointerId(pointerIndex); if (pointerId == mActivePointerId) { final int newPointerIndex = pointerIndex == 0 ? 1 : 0; mLastTouchX = event.getX(newPointerIndex); mLastTouchY = event.getY(newPointerIndex); mActivePointerId = event.getPointerId(newPointerIndex); } break; } return true; } @Override public boolean onInterceptTouchEvent(MotionEvent event) { if (mTopCard == null) { return false; } if (mGestureDetector.onTouchEvent(event)) { return true; } final int pointerIndex; final float x, y; final float dx, dy; switch (event.getActionMasked()) { case MotionEvent.ACTION_DOWN: mTopCard.getHitRect(childRect); CardModel cardModel = (CardModel)getAdapter().getItem(0); if (cardModel.getOnClickListener() != null) { cardModel.getOnClickListener().OnClickListener(); } pointerIndex = event.getActionIndex(); x = event.getX(pointerIndex); y = event.getY(pointerIndex); if (!childRect.contains((int) x, (int) y)) { return false; } mLastTouchX = x; mLastTouchY = y; mActivePointerId = event.getPointerId(pointerIndex); break; case MotionEvent.ACTION_MOVE: pointerIndex = event.findPointerIndex(mActivePointerId); x = event.getX(pointerIndex); y = event.getY(pointerIndex); if (Math.abs(x - mLastTouchX) > mTouchSlop || Math.abs(y - mLastTouchY) > mTouchSlop) { float[] points = new float[]{x - mTopCard.getLeft(), y - mTopCard.getTop()}; mTopCard.getMatrix().invert(mMatrix); mMatrix.mapPoints(points); mTopCard.setPivotX(points[0]); mTopCard.setPivotY(points[1]); return true; } } return false; } @Override public View getSelectedView() { throw new UnsupportedOperationException(); } @Override public void setSelection(int position) { throw new UnsupportedOperationException(); } public int getGravity() { return mGravity; } public void setGravity(int gravity) { mGravity = gravity; } public static class LayoutParams extends ViewGroup.LayoutParams { int viewType; public LayoutParams(Context c, AttributeSet attrs) { super(c, attrs); } public LayoutParams(int width, int height) { super(width, height); } public LayoutParams(ViewGroup.LayoutParams source) { super(source); } public LayoutParams(int w, int h, int viewType) { super(w, h); this.viewType = viewType; } } private class GestureListener extends SimpleOnGestureListener { @Override public boolean onFling(MotionEvent e1, MotionEvent e2, float velocityX, float velocityY) { Log.d("Fling", "Fling with " + velocityX + ", " + velocityY); final View topCard = mTopCard; float dx = e2.getX() - e1.getX(); if (Math.abs(dx) > mTouchSlop && Math.abs(velocityX) > Math.abs(velocityY) && Math.abs(velocityX) > mFlingSlop * 3) { float targetX = topCard.getX(); float targetY = topCard.getY(); long duration = 0; boundsRect.set(0 - topCard.getWidth() - 100, 0 - topCard.getHeight() - 100, getWidth() + 100, getHeight() + 100); while (boundsRect.contains((int) targetX, (int) targetY)) { targetX += velocityX / 10; targetY += velocityY / 10; duration += 100; } duration = Math.min(500, duration); mTopCard = getChildAt(getChildCount() - 2); CardModel cardModel = (CardModel)getAdapter().getItem(0); if(mTopCard != null) mTopCard.setLayerType(LAYER_TYPE_HARDWARE, null); if (cardModel.getOnCardDimissedListener() != null) { if ( targetX > 0 ) { cardModel.getOnCardDimissedListener().onDislike(); } else { cardModel.getOnCardDimissedListener().onLike(); } } topCard.animate() .setDuration(duration) .alpha(.75f) .setInterpolator(new LinearInterpolator()) .x(targetX) .y(targetY) .rotation(Math.copySign(45, velocityX)) .setListener(new AnimatorListenerAdapter() { @Override public void onAnimationEnd(Animator animation) { removeViewInLayout(topCard); ensureFull(); } @Override public void onAnimationCancel(Animator animation) { onAnimationEnd(animation); } }); return true; } else return false; } } }