如果你够酷的话你肯定知道cardslib,这是一个封装了各种CardView的和作为容器的CardListView,CardGridView的一个android控件库
CardListView中还提供了SwipToDismiss(滑动删除)的功能,十分炫酷,但是某些情况下很容易触发错误操作,而且在使用了viewpager的情况下更是噩梦,为此我们可以为它添加选项,让CardListView支持长按滑动删除。
首先,在cardslib目前版本中有个bug,就是CardListView在滑动删除过程中没有屏蔽掉多点操控,导致在滑动过程中可以通过另外一点上下滑动来强行终止swipe的过程,我们可以通过设置CardListView的MotionEventSplittingEnabled属性来修复这个bug(已经在github中提交了Pull Request ^_^ )
//CardListView.java protected void init(AttributeSet attrs, int defStyle){ //Init attrs initAttrs(attrs,defStyle); //Set divider to 0dp setDividerHeight(0); this.setMotionEventSplittingEnabled(false); }
为了将长按删除添加到CardListView中,我们需要捕获CardView的OnLongClick事件,并使用变量mLongClicked来标记使用已经长按,如果mLongClicked为true则开始swipe操作。但是这里需要注意的是我们需要用OnLongClick事件来处理swipetodismiss事件,所以我们不能为item再设置OnLongClickListener了。还有就是我们需要修改CardView的refreshCard方法,因为每次refreshCard之后会覆盖我们用来处理swipetodismiss的OnLongClickListener,我们需要修改下它,让它在refrehCard的时候不覆盖我们的OnLongClickListener;
package it.gmariotti.cardslib.library.view.listener; /* Copyright 2013 Roman Nurik, Gabriele Mariotti * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ import android.animation.Animator; import android.animation.AnimatorListenerAdapter; import android.animation.ValueAnimator; import android.graphics.Rect; import android.view.MotionEvent; import android.view.VelocityTracker; import android.view.View; import android.view.ViewConfiguration; import android.view.ViewGroup; import android.view.ViewPropertyAnimator; import android.widget.AbsListView; import android.widget.ListView; import java.util.ArrayList; import java.util.Collections; import java.util.List; import it.gmariotti.cardslib.library.internal.Card; /** * It is based on Roman Nurik code. * See this link for original code https://github.com/romannurik/Android-SwipeToDismiss * </p> * It provides a SwipeDismissViewTouchListener for a CardList. * </p> * * A {@link View.OnTouchListener} that makes the list items in a {@link ListView} * dismissable. {@link ListView} is given special treatment because by default it handles touches * for its list items... i.e. it‘s in charge of drawing the pressed state (the list selector), * handling list item clicks, etc. * * <p>After creating the listener, the caller should also call * {@link ListView#setOnScrollListener(AbsListView.OnScrollListener)}, passing * in the scroll listener returned by {@link #makeScrollListener()}. If a scroll listener is * already assigned, the caller should still pass scroll changes through to this listener. This will * ensure that this {@link SwipeDismissListViewTouchListener} is paused during list view * scrolling.</p> * * <p>Example usage:</p> * * <pre> * SwipeDismissListViewTouchListener touchListener = * new SwipeDismissListViewTouchListener( * listView, * new SwipeDismissListViewTouchListener.OnDismissCallback() { * public void onDismiss(ListView listView, int[] reverseSortedPositions) { * for (int position : reverseSortedPositions) { * adapter.remove(adapter.getItem(position)); * } * adapter.notifyDataSetChanged(); * } * }); * listView.setOnTouchListener(touchListener); * listView.setOnScrollListener(touchListener.makeScrollListener()); * </pre> * * <p>This class Requires API level 12 or later due to use of {@link * ViewPropertyAnimator}.</p> * */ public class SwipeDismissListViewTouchListener implements View.OnTouchListener, View.OnLongClickListener { // Cached ViewConfiguration and system-wide constant values private int mSlop; private int mMinFlingVelocity; private int mMaxFlingVelocity; private long mAnimationTime; // Fixed properties private ListView mListView; private DismissCallbacks mCallbacks; private int mViewWidth = 1; // 1 and not 0 to prevent dividing by zero // Transient properties private List<PendingDismissData> mPendingDismisses = new ArrayList<PendingDismissData>(); private int mDismissAnimationRefCount = 0; private float mDownX; private boolean mSwiping; private VelocityTracker mVelocityTracker; private int mDownPosition; private View mDownView; private boolean mPaused; private boolean mLongClicked = false; private boolean mUseLongClickSwipe = false; /** * The callback interface used by {@link SwipeDismissListViewTouchListener} to inform its client * about a successful dismissal of one or more list item positions. */ public interface DismissCallbacks { /** * Called to determine whether the given position can be dismissed. */ boolean canDismiss(int position,Card card); /** * Called when the user has indicated they she would like to dismiss one or more list item * positions. * * @param listView The originating {@link ListView}. * @param reverseSortedPositions An array of positions to dismiss, sorted in descending * order for convenience. */ void onDismiss(ListView listView, int[] reverseSortedPositions); } /** * Constructs a new swipe-to-dismiss touch listener for the given list view. * * @param listView The list view whose items should be dismissable. * @param callbacks The callback to trigger when the user has indicated that she would like to * dismiss one or more list items. */ public SwipeDismissListViewTouchListener(ListView listView, DismissCallbacks callbacks) { ViewConfiguration vc = ViewConfiguration.get(listView.getContext()); mSlop = vc.getScaledTouchSlop(); mMinFlingVelocity = vc.getScaledMinimumFlingVelocity() * 16; mMaxFlingVelocity = vc.getScaledMaximumFlingVelocity(); mAnimationTime = listView.getContext().getResources().getInteger( android.R.integer.config_shortAnimTime); mListView = listView; mCallbacks = callbacks; } /** * Enables or disables (pauses or resumes) watching for swipe-to-dismiss gestures. * * @param enabled Whether or not to watch for gestures. */ public void setEnabled(boolean enabled) { mPaused = !enabled; } public void setUseLongClickSwipe(boolean useLongClickSwipe) { mUseLongClickSwipe = useLongClickSwipe; } /** * Returns an {@link AbsListView.OnScrollListener} to be added to the {@link * ListView} using {@link ListView#setOnScrollListener(AbsListView.OnScrollListener)}. * If a scroll listener is already assigned, the caller should still pass scroll changes through * to this listener. This will ensure that this {@link SwipeDismissListViewTouchListener} is * paused during list view scrolling.</p> * * @see SwipeDismissListViewTouchListener */ public AbsListView.OnScrollListener makeScrollListener() { return new AbsListView.OnScrollListener() { @Override public void onScrollStateChanged(AbsListView absListView, int scrollState) { if(!mUseLongClickSwipe) setEnabled(scrollState != AbsListView.OnScrollListener.SCROLL_STATE_TOUCH_SCROLL); } @Override public void onScroll(AbsListView absListView, int i, int i1, int i2) { } }; } @Override public boolean onTouch(View view, MotionEvent motionEvent) { if (mViewWidth < 2) { mViewWidth = mListView.getWidth(); } switch (motionEvent.getActionMasked()) { case MotionEvent.ACTION_DOWN: { mLongClicked = false; if (mPaused) { return false; } // TODO: ensure this is a finger, and set a flag // Find the child view that was touched (perform a hit test) Rect rect = new Rect(); int childCount = mListView.getChildCount(); int[] listViewCoords = new int[2]; mListView.getLocationOnScreen(listViewCoords); int x = (int) motionEvent.getRawX() - listViewCoords[0]; int y = (int) motionEvent.getRawY() - listViewCoords[1]; View child=null; for (int i = 0; i < childCount; i++) { child = mListView.getChildAt(i); child.getHitRect(rect); if (rect.contains(x, y)) { mDownView = child; break; } } if (mDownView != null) { mDownX = motionEvent.getRawX(); mDownPosition = mListView.getPositionForView(mDownView); if (mCallbacks.canDismiss(mDownPosition,(Card) mListView.getAdapter().getItem(mDownPosition))) { mVelocityTracker = VelocityTracker.obtain(); mVelocityTracker.addMovement(motionEvent); } else { mDownView = null; } } view.onTouchEvent(motionEvent); return true; } case MotionEvent.ACTION_UP: { if (mVelocityTracker == null) { break; } mLongClicked = false; float deltaX = motionEvent.getRawX() - mDownX; mVelocityTracker.addMovement(motionEvent); mVelocityTracker.computeCurrentVelocity(1000); float velocityX = mVelocityTracker.getXVelocity(); float absVelocityX = Math.abs(velocityX); float absVelocityY = Math.abs(mVelocityTracker.getYVelocity()); boolean dismiss = false; boolean dismissRight = false; if (Math.abs(deltaX) > mViewWidth * 1/3) { dismiss = true; dismissRight = deltaX > 0; } else if (mMinFlingVelocity <= absVelocityX && absVelocityX <= mMaxFlingVelocity && absVelocityY < absVelocityX) { // dismiss only if flinging in the same direction as dragging dismiss = (velocityX < 0) == (deltaX < 0); dismissRight = mVelocityTracker.getXVelocity() > 0; } if (dismiss) { // dismiss final View downView = mDownView; // mDownView gets null‘d before animation ends final int downPosition = mDownPosition; ++mDismissAnimationRefCount; mDownView.animate() .translationX(dismissRight ? mViewWidth : -mViewWidth) .alpha(0) .setDuration(mAnimationTime) .setListener(new AnimatorListenerAdapter() { @Override public void onAnimationEnd(Animator animation) { performDismiss(downView, downPosition); } }); } else { // cancel mDownView.animate() .translationX(0) .alpha(1) .setDuration(mAnimationTime) .setListener(null); } mVelocityTracker.recycle(); mVelocityTracker = null; mDownX = 0; mDownView = null; mDownPosition = ListView.INVALID_POSITION; mSwiping = false; break; } case MotionEvent.ACTION_MOVE: { if (mVelocityTracker == null || mPaused || (mUseLongClickSwipe && !mLongClicked)) { break; } mVelocityTracker.addMovement(motionEvent); float deltaX = motionEvent.getRawX() - mDownX; if (Math.abs(deltaX) > mSlop) { mSwiping = true; mListView.requestDisallowInterceptTouchEvent(true); // Cancel ListView‘s touch (un-highlighting the item) MotionEvent cancelEvent = MotionEvent.obtain(motionEvent); cancelEvent.setAction(MotionEvent.ACTION_CANCEL | (motionEvent.getActionIndex() << MotionEvent.ACTION_POINTER_INDEX_SHIFT)); mListView.onTouchEvent(cancelEvent); cancelEvent.recycle(); } if (mSwiping) { mDownView.setTranslationX(deltaX); mDownView.setAlpha(Math.max(0f, Math.min(1f, 1f - 2f * Math.abs(deltaX) / mViewWidth))); return true; } break; } } return false; } class PendingDismissData implements Comparable<PendingDismissData> { public int position; public View view; public PendingDismissData(int position, View view) { this.position = position; this.view = view; } @Override public int compareTo(PendingDismissData other) { // Sort by descending position return other.position - position; } } private void performDismiss(final View dismissView, final int dismissPosition) { // Animate the dismissed list item to zero-height and fire the dismiss callback when // all dismissed list item animations have completed. This triggers layout on each animation // frame; in the future we may want to do something smarter and more performant. final ViewGroup.LayoutParams lp = dismissView.getLayoutParams(); final int originalHeight = dismissView.getHeight(); ValueAnimator animator = ValueAnimator.ofInt(originalHeight, 1).setDuration(mAnimationTime); animator.addListener(new AnimatorListenerAdapter() { @Override public void onAnimationEnd(Animator animation) { --mDismissAnimationRefCount; if (mDismissAnimationRefCount == 0) { // No active animations, process all pending dismisses. // Sort by descending position Collections.sort(mPendingDismisses); int[] dismissPositions = new int[mPendingDismisses.size()]; for (int i = mPendingDismisses.size() - 1; i >= 0; i--) { dismissPositions[i] = mPendingDismisses.get(i).position; } mCallbacks.onDismiss(mListView, dismissPositions); ViewGroup.LayoutParams lp; for (PendingDismissData pendingDismiss : mPendingDismisses) { // Reset view presentation pendingDismiss.view.setAlpha(1f); pendingDismiss.view.setTranslationX(0); lp = pendingDismiss.view.getLayoutParams(); lp.height = originalHeight; pendingDismiss.view.setLayoutParams(lp); } mPendingDismisses.clear(); } } }); animator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() { @Override public void onAnimationUpdate(ValueAnimator valueAnimator) { lp.height = (Integer) valueAnimator.getAnimatedValue(); dismissView.setLayoutParams(lp); } }); mPendingDismisses.add(new PendingDismissData(dismissPosition, dismissView)); animator.start(); } @Override public boolean onLongClick(View v) { mLongClicked = true; mDownView.setTranslationX(20); mListView.requestDisallowInterceptTouchEvent(true); return true; } }
/* * ****************************************************************************** * Copyright (c) 2013 Gabriele Mariotti. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * ***************************************************************************** */ package it.gmariotti.cardslib.library.internal; import android.app.Activity; import android.content.Context; import android.content.res.Resources; import android.os.Parcelable; import android.util.Log; import android.view.LayoutInflater; import android.view.View; import android.view.View.OnLongClickListener; import android.view.ViewGroup; import android.widget.AdapterView; import android.widget.AdapterView.OnItemLongClickListener; import android.widget.ListView; import java.util.HashMap; import java.util.List; import it.gmariotti.cardslib.library.R; import it.gmariotti.cardslib.library.internal.base.BaseCardArrayAdapter; import it.gmariotti.cardslib.library.view.CardListView; import it.gmariotti.cardslib.library.view.CardView; import it.gmariotti.cardslib.library.view.listener.SwipeDismissListViewTouchListener; import it.gmariotti.cardslib.library.view.listener.UndoBarController; import it.gmariotti.cardslib.library.view.listener.UndoCard; /** * Array Adapter for {@link Card} model * <p/> * Usage: * <pre><code> * ArrayList<Card> cards = new ArrayList<Card>(); * for (int i=0;i<1000;i++){ * CardExample card = new CardExample(getActivity(),"My title "+i,"Inner text "+i); * cards.add(card); * } * * CardArrayAdapter mCardArrayAdapter = new CardArrayAdapter(getActivity(),cards); * * CardListView listView = (CardListView) getActivity().findViewById(R.id.listId); * listView.setAdapter(mCardArrayAdapter); * * </code></pre> * It provides a default layout id for each row @layout/list_card_layout * Use can easily customize it using card:list_card_layout_resourceID attr in your xml layout: * <pre><code> * <it.gmariotti.cardslib.library.view.CardListView * android:layout_width="match_parent" * android:layout_height="match_parent" * android:id="@+id/carddemo_list_gplaycard" * card:list_card_layout_resourceID="@layout/list_card_thumbnail_layout" /> * </code></pre> * or: * <pre><code> * adapter.setRowLayoutId(list_card_layout_resourceID); * </code></pre> * </p> * @author Gabriele Mariotti (gabri.mariotti@gmail.com) */ public class CardArrayAdapter extends BaseCardArrayAdapter implements UndoBarController.UndoListener { protected static String TAG = "CardArrayAdapter"; /** * {@link CardListView} */ protected CardListView mCardListView; /** * Listener invoked when a card is swiped */ protected SwipeDismissListViewTouchListener mOnTouchListener; /** * Used to enable an undo message after a swipe action */ protected boolean mEnableUndo=false; protected boolean mUseLongClickSwipe = false; /** * Undo Controller */ protected UndoBarController mUndoBarController; /** * Internal Map with all Cards. * It uses the card id value as key. */ protected HashMap<String /* id */,Card> mInternalObjects; // ------------------------------------------------------------- // Constructors // ------------------------------------------------------------- /** * Constructor * * @param context The current context. * @param cards The cards to represent in the ListView. */ public CardArrayAdapter(Context context, List<Card> cards) { super(context, cards); } // ------------------------------------------------------------- // Views // ------------------------------------------------------------- @Override public View getView(int position, View convertView, ViewGroup parent) { View view = convertView; CardView mCardView; Card mCard; LayoutInflater mInflater = (LayoutInflater) mContext.getSystemService(Context.LAYOUT_INFLATER_SERVICE); //Retrieve card from items mCard = (Card) getItem(position); if (mCard != null) { int layout = mRowLayoutId; boolean recycle = false; //Inflate layout if (view == null) { recycle = false; view = mInflater.inflate(layout, parent, false); } else { recycle = true; } //Setup card mCardView = (CardView) view.findViewById(R.id.list_cardId); if (mCardView != null) { //It is important to set recycle value for inner layout elements mCardView.setForceReplaceInnerLayout(Card.equalsInnerLayout(mCardView.getCard(),mCard)); //It is important to set recycle value for performance issue mCardView.setRecycle(recycle); //Save original swipeable to prevent cardSwipeListener (listView requires another cardSwipeListener) //boolean origianlSwipeable = mCard.isSwipeable(); //mCard.setSwipeable(false); mCardView.setIsInCardListView(true); mCardView.setCard(mCard); //Set originalValue //mCard.setSwipeable(origianlSwipeable); //If card has an expandable button override animation if (mCard.getCardHeader() != null && mCard.getCardHeader().isButtonExpandVisible()) { setupExpandCollapseListAnimation(mCardView); } //Setup swipeable animation setupSwipeableAnimation(mCard, mCardView); //setupMultiChoice setupMultichoice(view,mCard,mCardView,position); } } return view; } /** * Sets SwipeAnimation on List * * @param card {@link Card} * @param cardView {@link CardView} */ protected void setupSwipeableAnimation(final Card card, CardView cardView) { if (card.isSwipeable()){ if (mOnTouchListener == null){ mOnTouchListener = new SwipeDismissListViewTouchListener(mCardListView, mCallback); // Setting this scroll listener is required to ensure that during // ListView scrolling, we don‘t look for swipes. mCardListView.setOnScrollListener(mOnTouchListener.makeScrollListener()); } mOnTouchListener.setUseLongClickSwipe(mUseLongClickSwipe); cardView.setOnTouchListener(mOnTouchListener); if(mUseLongClickSwipe) { cardView.setOnLongClickListener(mOnTouchListener); } }else{ //prevent issue with recycle view cardView.setOnTouchListener(null); } } public void setUseLongClickSwipe(boolean useLongClickSwipe) { mUseLongClickSwipe = useLongClickSwipe; } /** * Overrides the default collapse/expand animation in a List * * @param cardView {@link CardView} */ protected void setupExpandCollapseListAnimation(CardView cardView) { if (cardView == null) return; cardView.setOnExpandListAnimatorListener(mCardListView); } // ------------------------------------------------------------- // SwipeListener and undo action // ------------------------------------------------------------- /** * Listener invoked when a card is swiped */ SwipeDismissListViewTouchListener.DismissCallbacks mCallback = new SwipeDismissListViewTouchListener.DismissCallbacks() { @Override public boolean canDismiss(int position, Card card) { return card.isSwipeable(); } @Override public void onDismiss(ListView listView, int[] reverseSortedPositions) { int[] itemPositions=new int[reverseSortedPositions.length]; String[] itemIds=new String[reverseSortedPositions.length]; int i=0; //Remove cards and notifyDataSetChanged for (int position : reverseSortedPositions) { Card card = getItem(position); itemPositions[i]=position; itemIds[i]=card.getId(); i++; remove(card); if (card.getOnSwipeListener() != null){ card.getOnSwipeListener().onSwipe(card); } } notifyDataSetChanged(); //Check for a undo message to confirm if (isEnableUndo() && mUndoBarController!=null){ //Show UndoBar UndoCard itemUndo=new UndoCard(itemPositions,itemIds); if (getContext()!=null){ Resources res = getContext().getResources(); if (res!=null){ String messageUndoBar = res.getQuantityString(R.plurals.list_card_undo_items, reverseSortedPositions.length, reverseSortedPositions.length); mUndoBarController.showUndoBar( false, messageUndoBar, itemUndo); } } } } }; // ------------------------------------------------------------- // Undo Default Listener // ------------------------------------------------------------- @Override public void onUndo(Parcelable token) { //Restore items in lists (use reverseSortedOrder) if (token != null) { UndoCard item = (UndoCard) token; int[] itemPositions = item.itemPosition; String[] itemIds = item.itemId; if (itemPositions != null) { int end = itemPositions.length; for (int i = end - 1; i >= 0; i--) { int itemPosition = itemPositions[i]; String id= itemIds[i]; if (id==null){ Log.w(TAG, "You have to set a id value to use the undo action"); }else{ Card card = mInternalObjects.get(id); if (card!=null){ insert(card, itemPosition); notifyDataSetChanged(); if (card.getOnUndoSwipeListListener()!=null) card.getOnUndoSwipeListListener().onUndoSwipe(card); } } } } } } // ------------------------------------------------------------- // Getters and Setters // ------------------------------------------------------------- /** * @return {@link CardListView} */ public CardListView getCardListView() { return mCardListView; } /** * Sets the {@link CardListView} * * @param cardListView cardListView */ public void setCardListView(CardListView cardListView) { this.mCardListView = cardListView; } /** * Indicates if the undo message is enabled after a swipe action * * @return <code>true</code> if the undo message is enabled */ public boolean isEnableUndo() { return mEnableUndo; } /** * Enables an undo message after a swipe action * * @param enableUndo <code>true</code> to enable an undo message */ public void setEnableUndo(boolean enableUndo) { mEnableUndo = enableUndo; if (enableUndo) { mInternalObjects = new HashMap<String, Card>(); for (int i=0;i<getCount();i++) { Card card = getItem(i); mInternalObjects.put(card.getId(), card); } //Create a UndoController if (mUndoBarController==null){ View undobar = ((Activity)mContext).findViewById(R.id.list_card_undobar); if (undobar != null) { mUndoBarController = new UndoBarController(undobar, this); } } }else{ mUndoBarController=null; } } /** * Return the UndoBarController for undo action * * @return {@link UndoBarController} */ public UndoBarController getUndoBarController() { return mUndoBarController; } }
/* * ****************************************************************************** * Copyright (c) 2013 Gabriele Mariotti. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * ***************************************************************************** */ package it.gmariotti.cardslib.library.view; import android.animation.Animator; import android.animation.AnimatorListenerAdapter; import android.animation.ValueAnimator; import android.annotation.SuppressLint; import android.content.Context; import android.content.res.TypedArray; import android.graphics.Bitmap; import android.graphics.Canvas; import android.graphics.drawable.Drawable; import android.os.Build; import android.util.AttributeSet; import android.view.View; import android.view.ViewGroup; import android.view.ViewTreeObserver; import java.util.HashMap; import it.gmariotti.cardslib.library.R; import it.gmariotti.cardslib.library.internal.Card; import it.gmariotti.cardslib.library.internal.CardExpand; import it.gmariotti.cardslib.library.internal.CardHeader; import it.gmariotti.cardslib.library.internal.CardThumbnail; import it.gmariotti.cardslib.library.view.component.CardHeaderView; import it.gmariotti.cardslib.library.view.component.CardThumbnailView; import it.gmariotti.cardslib.library.view.listener.SwipeDismissViewTouchListener; /** * Card view * </p> * Use an XML layout file to display it. * </p> * First, you need an XML layout that will display the Card. * <pre><code> * <it.gmariotti.cardslib.library.view.CardView * android:id="@+id/carddemo_example_card3" * android:layout_width="match_parent" * android:layout_height="wrap_content" * android:layout_marginLeft="12dp" * android:layout_marginRight="12dp" * android:layout_marginTop="12dp"/> * </code></pre> * Then create a model: * <pre><code> * * //Create a Card * Card card = new Card(getContext()); * * //Create a CardHeader * CardHeader header = new CardHeader(getContext()); * * //Add Header to card * card.addCardHeader(header); * * </code></pre> * Last get a reference to the `CardView` from your code, and set your `Card. * <pre><code> * //Set card in the cardView * CardView cardView = (CardView) getActivity().findViewById(R.id.carddemo); * * cardView.setCard(card); * </code></pre> * You can easily build your layout. * </p> * The quickest way to start with this would be to copy one of this files and create your layout. * Then you can inflate your layout in the `CardView` using the attr: `card:card_layout_resourceID="@layout/my_layout` * Example: * <pre><code> * <it.gmariotti.cardslib.library.view.CardView * android:id="@+id/carddemo_thumb_url" * android:layout_width="match_parent" * android:layout_height="wrap_content" * android:layout_marginLeft="12dp" * android:layout_marginRight="12dp" * card:card_layout_resourceID="@layout/card_thumbnail_layout" * android:layout_marginTop="12dp"/> * </code></pre> * </p> * @author Gabriele Mariotti (gabri.mariotti@gmail.com) */ public class CardView extends BaseCardView { //-------------------------------------------------------------------------- // //-------------------------------------------------------------------------- /** * {@link CardHeader} model */ protected CardHeader mCardHeader; /** * {@link CardThumbnail} model */ protected CardThumbnail mCardThumbnail; /** * {@link CardExpand} model */ protected CardExpand mCardExpand; //-------------------------------------------------------------------------- // Layout //-------------------------------------------------------------------------- /** * Main Layout */ protected View mInternalMainCardLayout; /** * Content Layout */ protected View mInternalContentLayout; /** * Inner View. */ protected View mInternalInnerView; /** * Hidden layout used by expand/collapse action */ protected View mInternalExpandLayout; /** * Expand Inner view */ protected View mInternalExpandInnerView; /** Animator to expand/collapse */ protected Animator mExpandAnimator; /** * Listener invoked when Expand Animator starts * It is used internally */ protected OnExpandListAnimatorListener mOnExpandListAnimatorListener; //-------------------------------------------------------------------------- // Constructor //-------------------------------------------------------------------------- public CardView(Context context) { super(context); } public CardView(Context context, AttributeSet attrs) { super(context, attrs); } public CardView(Context context, AttributeSet attrs, int defStyle) { super(context, attrs, defStyle); } //-------------------------------------------------------------------------- // Init //-------------------------------------------------------------------------- /** * Init custom attrs. * * @param attrs * @param defStyle */ protected void initAttrs(AttributeSet attrs, int defStyle) { card_layout_resourceID = R.layout.card_layout; TypedArray a = getContext().getTheme().obtainStyledAttributes( attrs, R.styleable.card_options, defStyle, defStyle); try { card_layout_resourceID = a.getResourceId(R.styleable.card_options_card_layout_resourceID, this.card_layout_resourceID); } finally { a.recycle(); } } //-------------------------------------------------------------------------- // Card //-------------------------------------------------------------------------- /** * Add a {@link Card}. * It is very important to set all values and all components before launch this method. * * @param card {@link Card} model */ @Override public void setCard(Card card){ super.setCard(card); if (card!=null){ mCardHeader=card.getCardHeader(); mCardThumbnail=card.getCardThumbnail(); mCardExpand=card.getCardExpand(); } //Retrieve all IDs if (!isRecycle()){ retrieveLayoutIDs(); } //Build UI buildUI(); } /** * Refreshes the card content (it doesn‘t inflate layouts again) * * @param card */ public void refreshCard(Card card) { if(mIsInCardListView) mPassSetupListener = true; mIsRecycle=true; setCard(card); mIsRecycle=false; mPassSetupListener = false; } private boolean mIsInCardListView = false; private boolean mPassSetupListener = false; public void setIsInCardListView(boolean isInCardListView) { mIsInCardListView = isInCardListView; } /** * Refreshes the card content and replaces the inner layout elements (it inflates layouts again!) * * @param card */ public void replaceCard(Card card) { mForceReplaceInnerLayout=true; refreshCard(card); mForceReplaceInnerLayout=false; } //-------------------------------------------------------------------------- // Setup methods //-------------------------------------------------------------------------- @Override protected void buildUI() { super.buildUI(); mCard.setCardView(this); //Setup Header view setupHeaderView(); //Setup Main View setupMainView(); //setup Thumbnail setupThumbnailView(); //Setup Expand View setupExpandView(); if(!mPassSetupListener) //Setup Listeners setupListeners(); //Setup Drawable Resources setupDrawableResources(); } /** * Retrieve all Layouts IDs */ @Override protected void retrieveLayoutIDs(){ super.retrieveLayoutIDs(); //Main Layout mInternalMainCardLayout = (View) findViewById(R.id.card_main_layout); //Get HeaderLayout mInternalHeaderLayout = (CardHeaderView) findViewById(R.id.card_header_layout); //Get ExpandHiddenView mInternalExpandLayout = (View) findViewById(R.id.card_content_expand_layout); //Get ContentLayout mInternalContentLayout = (View) findViewById(R.id.card_main_content_layout); //Get ThumbnailLayout mInternalThumbnailLayout = (CardThumbnailView) findViewById(R.id.card_thumbnail_layout); } /** * Setup Header View */ protected void setupHeaderView(){ if (mCardHeader!=null){ if (mInternalHeaderLayout !=null){ mInternalHeaderLayout.setVisibility(VISIBLE); //Set recycle value (very important in a ListView) mInternalHeaderLayout.setRecycle(isRecycle()); mInternalHeaderLayout.setForceReplaceInnerLayout(isForceReplaceInnerLayout()); //Add Header View mInternalHeaderLayout.addCardHeader(mCardHeader); //Config ExpandLayout and its animation if (mInternalExpandLayout !=null && mCardHeader.isButtonExpandVisible() ){ //Create the expand/collapse animator mInternalExpandLayout.getViewTreeObserver().addOnPreDrawListener( new ViewTreeObserver.OnPreDrawListener() { @Override public boolean onPreDraw() { mInternalExpandLayout.getViewTreeObserver().removeOnPreDrawListener(this); //mInternalExpandLayout.setVisibility(View.GONE); final int widthSpec = View.MeasureSpec.makeMeasureSpec(0, View.MeasureSpec.UNSPECIFIED); final int heightSpec = View.MeasureSpec.makeMeasureSpec(0, View.MeasureSpec.UNSPECIFIED); mInternalExpandLayout.measure(widthSpec, heightSpec); final int widthSpecCard = View.MeasureSpec.makeMeasureSpec(0, View.MeasureSpec.UNSPECIFIED); final int heightSpecCard = View.MeasureSpec.makeMeasureSpec(0, View.MeasureSpec.UNSPECIFIED); mCollapsedHeight = getMeasuredHeight(); mExpandAnimator = createSlideAnimator(0, mInternalExpandLayout.getMeasuredHeight()); return true; } }); } //Setup action and callback setupExpandCollapseAction(); } }else{ //No header. Hide layouts if (mInternalHeaderLayout !=null){ mInternalHeaderLayout.setVisibility(GONE); mInternalExpandLayout.setVisibility(View.GONE); if (isForceReplaceInnerLayout()){ mInternalHeaderLayout.addCardHeader(null); //mInternalHeaderLayout.removeAllViews(); } } } } /** * Setup the Main View */ protected void setupMainView(){ if (mInternalContentLayout !=null){ ViewGroup mParentGroup=null; try{ mParentGroup = (ViewGroup) mInternalContentLayout; }catch (Exception e){ setRecycle(false); } //Check if view can be recycled //It can happen in a listView, and improves performances if (!isRecycle() || isForceReplaceInnerLayout()){ if (isForceReplaceInnerLayout() && mInternalContentLayout!=null && mInternalInnerView!=null) ((ViewGroup)mInternalContentLayout).removeView(mInternalInnerView); mInternalInnerView=mCard.getInnerView(getContext(), (ViewGroup) mInternalContentLayout); }else{ //View can be recycled. //Only setup Inner Elements if (mCard.getInnerLayout()>-1) mCard.setupInnerViewElements(mParentGroup,mInternalInnerView); } } } /** * Setup the Thumbnail View */ protected void setupThumbnailView() { if (mInternalThumbnailLayout!=null){ if (mCardThumbnail!=null){ mInternalThumbnailLayout.setVisibility(VISIBLE); mInternalThumbnailLayout.setRecycle(isRecycle()); mInternalThumbnailLayout.setForceReplaceInnerLayout(isForceReplaceInnerLayout()); mInternalThumbnailLayout.addCardThumbnail(mCardThumbnail); }else{ mInternalThumbnailLayout.setVisibility(GONE); } } } /** * Setup Drawable Resources */ protected void setupDrawableResources() { //Card if (mCard!=null){ if (mCard.getBackgroundResourceId()!=0){ changeBackgroundResourceId(mCard.getBackgroundResourceId()); }else if (mCard.getBackgroundResource()!=null){ changeBackgroundResource(mCard.getBackgroundResource()); } } } //-------------------------------------------------------------------------- // Listeners //-------------------------------------------------------------------------- /** * Setup All listeners */ @SuppressWarnings("deprecation") @SuppressLint("NewApi") protected void setupListeners(){ //Swipe listener if (mCard.isSwipeable() && !mIsInCardListView){ this.setOnTouchListener(new SwipeDismissViewTouchListener(this, mCard,new SwipeDismissViewTouchListener.DismissCallbacks() { @Override public boolean canDismiss(Card card) { return card.isSwipeable(); } @Override public void onDismiss(CardView cardView, Card card) { final ViewGroup vg = (ViewGroup)(cardView.getParent()); if (vg!=null){ vg.removeView(cardView); card.onSwipeCard(); } } })); }else{ this.setOnTouchListener(null); } //OnClick listeners and partial listener //Reset Partial Listeners resetPartialListeners(); if (mCard.isClickable()){ //Set the onClickListener if(!mCard.isMultiChoiceEnabled()){ if (mCard.getOnClickListener() != null) { this.setOnClickListener(new OnClickListener() { @Override public void onClick(View v) { if (mCard.getOnClickListener()!=null) mCard.getOnClickListener().onClick(mCard,v); } }); //Prevent multiple events //if (!mCard.isSwipeable() && mCard.getOnSwipeListener() == null) { // this.setClickable(true); //} }else{ HashMap<Integer,Card.OnCardClickListener> mMultipleOnClickListner=mCard.getMultipleOnClickListener(); if (mMultipleOnClickListner!=null && !mMultipleOnClickListner.isEmpty()){ for (int key:mMultipleOnClickListner.keySet()){ View viewClickable= decodeAreaOnClickListener(key); final Card.OnCardClickListener mListener=mMultipleOnClickListner.get(key); if (viewClickable!=null){ //Add listener to this view viewClickable.setOnClickListener(new OnClickListener() { @Override public void onClick(View v) { //Callback to card listener if (mListener!=null) mListener.onClick(mCard,v); } }); //Add Selector to this view if (key > Card.CLICK_LISTENER_ALL_VIEW) { if (Build.VERSION.SDK_INT >= 16){ viewClickable.setBackground(getResources().getDrawable(R.drawable.card_selector)); } else { viewClickable.setBackgroundDrawable(getResources().getDrawable(R.drawable.card_selector)); } } } } }else{ //There aren‘t listners this.setClickable(false); } } } }else{ this.setClickable(false); } //LongClick listener if(mCard.isLongClickable()){ this.setOnLongClickListener(new OnLongClickListener() { @Override public boolean onLongClick(View v) { if (mCard.getOnLongClickListener()!=null) return mCard.getOnLongClickListener().onLongClick(mCard,v); return false; } }); }else{ this.setLongClickable(false); } } /** * Reset all partial listeners */ protected void resetPartialListeners() { View viewClickable= decodeAreaOnClickListener(Card.CLICK_LISTENER_HEADER_VIEW); if (viewClickable!=null) viewClickable.setClickable(false); viewClickable= decodeAreaOnClickListener(Card.CLICK_LISTENER_THUMBNAIL_VIEW); if (viewClickable!=null) viewClickable.setClickable(false); viewClickable= decodeAreaOnClickListener(Card.CLICK_LISTENER_CONTENT_VIEW); if (viewClickable!=null) viewClickable.setClickable(false); } /** * * @param area * @return */ protected View decodeAreaOnClickListener(int area){ if (area<Card.CLICK_LISTENER_ALL_VIEW && area>Card.CLICK_LISTENER_CONTENT_VIEW) return null; View view = null; switch (area){ case Card.CLICK_LISTENER_ALL_VIEW : view=this; break; case Card.CLICK_LISTENER_HEADER_VIEW : view=mInternalHeaderLayout; break; case Card.CLICK_LISTENER_THUMBNAIL_VIEW: view=mInternalThumbnailLayout; break; case Card.CLICK_LISTENER_CONTENT_VIEW: view=mInternalContentLayout; break; default: break; } return view; } //-------------------------------------------------------------------------- // Expandable Actions and Listeners //-------------------------------------------------------------------------- protected int mCollapsedHeight; protected int mExpandedHeight=-1; /** * Add ClickListener to expand and collapse hidden view */ protected void setupExpandCollapseAction() { if (mInternalExpandLayout!=null){ mInternalExpandLayout.setVisibility(View.GONE); if (mCardHeader!=null){ if (mCardHeader.isButtonExpandVisible()){ mInternalHeaderLayout.setOnClickExpandCollapseActionListener(new TitleViewOnClickListener(mInternalExpandLayout,mCard)); if (isExpanded()){ //Make layout visible and button selected mInternalExpandLayout.setVisibility(View.VISIBLE); if(mInternalHeaderLayout.getImageButtonExpand()!=null) mInternalHeaderLayout.getImageButtonExpand().setSelected(true); }else{ //Make layout hidden and button not selected mInternalExpandLayout.setVisibility(View.GONE); if(mInternalHeaderLayout.getImageButtonExpand()!=null) mInternalHeaderLayout.getImageButtonExpand().setSelected(false); } } } } } /** * Setup Expand View */ protected void setupExpandView(){ if (mInternalExpandLayout!=null && mCardExpand!=null){ //Check if view can be recycled //It can happen in a listView, and improves performances if (!isRecycle() || isForceReplaceInnerLayout()){ if (isForceReplaceInnerLayout() && mInternalExpandLayout!=null && mInternalExpandInnerView!=null) ((ViewGroup)mInternalExpandLayout).removeView(mInternalExpandInnerView); mInternalExpandInnerView=mCardExpand.getInnerView(getContext(),(ViewGroup) mInternalExpandLayout); }else{ //View can be recycled. //Only setup Inner Elements if (mCardExpand.getInnerLayout()>-1) mCardExpand.setupInnerViewElements((ViewGroup)mInternalExpandLayout,mInternalExpandInnerView); } } } /** * Listener to expand/collapse hidden Expand Layout * It starts animation */ protected class TitleViewOnClickListener implements View.OnClickListener { private View mContentParent; private Card mCard; private TitleViewOnClickListener(View contentParent,Card card) { this.mContentParent = contentParent; this.mCard=card; } @Override public void onClick(View view) { boolean isVisible = mContentParent.getVisibility() == View.VISIBLE; if (isVisible) { animateCollapsing(); view.setSelected(false); } else { animateExpanding(); view.setSelected(true); } } /** * Expanding animator. */ private void animateExpanding() { if (getOnExpandListAnimatorListener()!=null){ //List Animator getOnExpandListAnimatorListener().onExpandStart(mCard.getCardView(), mContentParent); }else{ //Std animator mContentParent.setVisibility(View.VISIBLE); mExpandAnimator.addListener(new AnimatorListenerAdapter() { @Override public void onAnimationEnd(Animator animation) { mCard.setExpanded(true); //Callback if (mCard.getOnExpandAnimatorEndListener()!=null) mCard.getOnExpandAnimatorEndListener().onExpandEnd(mCard); } }); mExpandAnimator.start(); } } /** * Collapse animator */ private void animateCollapsing() { if (getOnExpandListAnimatorListener()!=null){ //There is a List Animator. getOnExpandListAnimatorListener().onCollapseStart(mCard.getCardView(), mContentParent); }else{ //Std animator int origHeight = mContentParent.getHeight(); ValueAnimator animator = createSlideAnimator(origHeight, 0); animator.addListener(new Animator.AnimatorListener() { @Override public void onAnimationStart(Animator animator) { } @Override public void onAnimationEnd(Animator animator) { mContentParent.setVisibility(View.GONE); mCard.setExpanded(false); //Callback if (mCard.getOnCollapseAnimatorEndListener()!=null) mCard.getOnCollapseAnimatorEndListener().onCollapseEnd(mCard); } @Override public void onAnimationCancel(Animator animator) { } @Override public void onAnimationRepeat(Animator animator) { } }); animator.start(); } } } /** * Create the Slide Animator invoked when the expand/collapse button is clicked */ protected ValueAnimator createSlideAnimator(int start, int end) { ValueAnimator animator = ValueAnimator.ofInt(start, end); animator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() { @Override public void onAnimationUpdate(ValueAnimator valueAnimator) { int value = (Integer) valueAnimator.getAnimatedValue(); ViewGroup.LayoutParams layoutParams = mInternalExpandLayout.getLayoutParams(); layoutParams.height = value; mInternalExpandLayout.setLayoutParams(layoutParams); } }); return animator; } @Override protected void onSizeChanged(int xNew, int yNew, int xOld, int yOld) { super.onSizeChanged(xNew, yNew, xOld, yOld); mExpandedHeight = yNew; } // ------------------------------------------------------------- // OnExpandListAnimator Interface and Listener // ------------------------------------------------------------- /** * Interface to listen any callbacks when expand/collapse animation starts */ public interface OnExpandListAnimatorListener { public void onExpandStart(CardView viewCard,View expandingLayout); public void onCollapseStart(CardView viewCard,View expandingLayout); } /** * Returns the listener invoked when expand/collpase animation starts * It is used internally * * @return listener */ public OnExpandListAnimatorListener getOnExpandListAnimatorListener() { return mOnExpandListAnimatorListener; } /** * Sets the listener invoked when expand/collapse animation starts * It is used internally. Don‘t override it. * * @param onExpandListAnimatorListener listener */ public void setOnExpandListAnimatorListener(OnExpandListAnimatorListener onExpandListAnimatorListener) { this.mOnExpandListAnimatorListener = onExpandListAnimatorListener; } // ------------------------------------------------------------- // Bitmap export // ------------------------------------------------------------- /** * Create a {@link android.graphics.Bitmap} from CardView * @return */ public Bitmap createBitmap(){ if (getWidth()<=0 && getHeight()<=0){ int spec = MeasureSpec.makeMeasureSpec( 0,MeasureSpec.UNSPECIFIED); measure(spec,spec); layout(0, 0, getMeasuredWidth(), getMeasuredHeight()); } Bitmap b = Bitmap.createBitmap(getWidth(), getHeight(), Bitmap.Config.ARGB_8888); Canvas c = new Canvas(b); draw(c); return b; } // ------------------------------------------------------------- // Getter and Setter // ------------------------------------------------------------- /** * Returns the view used by Expand Layout * * @return {@link View} used by Expand Layout */ public View getInternalExpandLayout() { return mInternalExpandLayout; } public int getCollapsedHeight() { return mCollapsedHeight; } public void setCollapsedHeight(int collapsedHeight) { mCollapsedHeight = collapsedHeight; } public int getExpandedHeight() { return mExpandedHeight; } public void setExpandedHeight(int expandedHeight) { mExpandedHeight = expandedHeight; } /** * Indicates if the card is expanded or collapsed * * @return <code>true</code> if the card is expanded */ public boolean isExpanded() { if (mCard!=null){ return mCard.isExpanded(); }else return false; } /** * Sets the card as expanded or collapsed * * @param expanded <code>true</code> if the card is expanded */ public void setExpanded(boolean expanded) { if (mCard!=null){ mCard.setExpanded(expanded); } } /** * Retrieves the InternalMainCardGlobalLayout. * Background style is applied here. * * @return */ public View getInternalMainCardLayout() { return mInternalMainCardLayout; } /** * Changes dynamically the drawable resource to override the style of MainLayout. * * @param drawableResourceId drawable resource Id */ public void changeBackgroundResourceId(int drawableResourceId) { if (drawableResourceId!=0){ if (mInternalMainCardLayout!=null){ mInternalMainCardLayout.setBackgroundResource(drawableResourceId); } } } /** * Changes dynamically the drawable resource to override the style of MainLayout. * * @param drawableResource drawable resource */ @SuppressLint("NewApi") public void changeBackgroundResource(Drawable drawableResource) { if (drawableResource!=null){ if (mInternalMainCardLayout!=null){ if(Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN) mInternalMainCardLayout.setBackground(drawableResource); else mInternalMainCardLayout.setBackgroundDrawable(drawableResource); } } } }
修改完这三个类之后我们可以很方便的使用setUseLongClickSwipe来设置是否使用长按来触发SwipeToDismiss操作~
card_array_adapter = new CardArrayAdapter(this.getActivity(), list_todo_card); card_array_adapter.setUseLongClickSwipe(true); CardListView cardlistview = (CardListView)view.findViewById(R.id.cardlist); cardlistview.setAdapter(card_array_adapter);