您的位置:首页 > 移动开发 > Android开发

为cardslib添加长按滑动删除(Android)

2020-07-13 04:16 344 查看

        如果你够酷的话你肯定知道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);
}
[/code]
        为了将长按删除添加到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;
}
}
[/code]
/*
* ******************************************************************************
*   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;
}
}
[/code]
/*
* ******************************************************************************
*   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);
}
}
}
}
[/code]

        修改完这三个类之后我们可以很方便的使用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);
[/code]

转载于:https://my.oschina.net/gal/blog/200166

内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: