如何使我的 Android Swipeable CardView 更像 IOS 7 邮件应用程序(滑动以显示按钮)
Posted
技术标签:
【中文标题】如何使我的 Android Swipeable CardView 更像 IOS 7 邮件应用程序(滑动以显示按钮)【英文标题】:How can I make my Android SwipeableCardViews more like the IOS 7 mail app (swipe to show buttons) 【发布时间】:2015-03-22 08:40:37 【问题描述】:如何让我的SwipeableCardViews
更像 ios 7 邮件应用程序(滑动显示按钮)
到目前为止,我已经创建了一个允许用户向左或向右滑动 Cardviews
的 android 应用程序。每张卡有 2 个按钮,稍后我将为其分配功能。
(如下图所示):
我想要完成的是(但还不知道如何),而不是向左或向右滑动以完全移除卡片。
相反,我希望通过滑动手势来显示隐藏在 right 或 left 卡片下方的按钮。
IOS 7 邮件应用程序已经执行此功能(截图如下)我想在 Android 中使用 Cardviews
(不是 ListViews
) 执行此功能。
我该怎么做?
到目前为止我的代码:
MainActivity.java
public class MainActivity extends ActionBarActivity
private RecyclerView mRecyclerView;
private CardViewAdapter mAdapter;
private ArrayList<String> mItems;
@Override
protected void onCreate(Bundle savedInstanceState)
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
mItems = new ArrayList<>(30);
for (int i = 0; i < 30; i++)
mItems.add(String.format("Card number %2d", i));
OnItemTouchListener itemTouchListener = new OnItemTouchListener()
@Override
public void onCardViewTap(View view, int position)
Toast.makeText(MainActivity.this, "Tapped " + mItems.get(position), Toast.LENGTH_SHORT).show();
@Override
public void onButton1Click(View view, int position)
Toast.makeText(MainActivity.this, "Clicked Button1 in " + mItems.get(position), Toast.LENGTH_SHORT).show();
@Override
public void onButton2Click(View view, int position)
Toast.makeText(MainActivity.this, "Clicked Button2 in " + mItems.get(position), Toast.LENGTH_SHORT).show();
;
mAdapter = new CardViewAdapter(mItems, itemTouchListener);
mRecyclerView = (RecyclerView) findViewById(R.id.recycler_view);
mRecyclerView.setLayoutManager(new LinearLayoutManager(this));
mRecyclerView.setAdapter(mAdapter);
SwipeableRecyclerViewTouchListener swipeTouchListener =
new SwipeableRecyclerViewTouchListener(mRecyclerView,
new SwipeableRecyclerViewTouchListener.SwipeListener()
@Override
public boolean canSwipe(int position)
return true;
@Override
public void onDismissedBySwipeLeft(RecyclerView recyclerView, int[] reverseSortedPositions)
for (int position : reverseSortedPositions)
mItems.remove(position);
mAdapter.notifyItemRemoved(position);
mAdapter.notifyDataSetChanged();
@Override
public void onDismissedBySwipeRight(RecyclerView recyclerView, int[] reverseSortedPositions)
for (int position : reverseSortedPositions)
mItems.remove(position);
mAdapter.notifyItemRemoved(position);
mAdapter.notifyDataSetChanged();
);
mRecyclerView.addOnItemTouchListener(swipeTouchListener);
/**
* Interface for the touch events in each item
*/
public interface OnItemTouchListener
/**
* Callback invoked when the user Taps one of the RecyclerView items
*
* @param view the CardView touched
* @param position the index of the item touched in the RecyclerView
*/
public void onCardViewTap(View view, int position);
/**
* Callback invoked when the Button1 of an item is touched
*
* @param view the Button touched
* @param position the index of the item touched in the RecyclerView
*/
public void onButton1Click(View view, int position);
/**
* Callback invoked when the Button2 of an item is touched
*
* @param view the Button touched
* @param position the index of the item touched in the RecyclerView
*/
public void onButton2Click(View view, int position);
/**
* A simple adapter that loads a CardView layout with one TextView and two Buttons, and
* listens to clicks on the Buttons or on the CardView
*/
public class CardViewAdapter extends RecyclerView.Adapter<CardViewAdapter.ViewHolder>
private List<String> cards;
private OnItemTouchListener onItemTouchListener;
public CardViewAdapter(List<String> cards, OnItemTouchListener onItemTouchListener)
this.cards = cards;
this.onItemTouchListener = onItemTouchListener;
@Override
public ViewHolder onCreateViewHolder(ViewGroup viewGroup, int i)
View v = LayoutInflater.from(viewGroup.getContext()).inflate(R.layout.card_view_layout, viewGroup, false);
return new ViewHolder(v);
@Override
public void onBindViewHolder(ViewHolder viewHolder, int i)
viewHolder.title.setText(cards.get(i));
@Override
public int getItemCount()
return cards == null ? 0 : cards.size();
public class ViewHolder extends RecyclerView.ViewHolder
private TextView title;
private Button button1;
private Button button2;
public ViewHolder(View itemView)
super(itemView);
title = (TextView) itemView.findViewById(R.id.card_view_title);
button1 = (Button) itemView.findViewById(R.id.card_view_button1);
button2 = (Button) itemView.findViewById(R.id.card_view_button2);
button1.setOnClickListener(new View.OnClickListener()
@Override
public void onClick(View v)
onItemTouchListener.onButton1Click(v, getPosition());
);
button2.setOnClickListener(new View.OnClickListener()
@Override
public void onClick(View v)
onItemTouchListener.onButton2Click(v, getPosition());
);
itemView.setOnClickListener(new View.OnClickListener()
@Override
public void onClick(View v)
onItemTouchListener.onCardViewTap(v, getPosition());
);
SwipeableRecyclerViewTouchListener.java
public class SwipeableRecyclerViewTouchListener implements RecyclerView.OnItemTouchListener
// Cached ViewConfiguration and system-wide constant values
private int mSlop;
private int mMinFlingVelocity;
private int mMaxFlingVelocity;
private long mAnimationTime;
// Fixed properties
private RecyclerView mRecyclerView;
private SwipeListener mSwipeListener;
private int mViewWidth = 1; // 1 and not 0 to prevent dividing by zero
// Transient properties
private List<PendingDismissData> mPendingDismisses = new ArrayList<>();
private int mDismissAnimationRefCount = 0;
private float mDownX;
private float mDownY;
private boolean mSwiping;
private int mSwipingSlop;
private VelocityTracker mVelocityTracker;
private int mDownPosition;
private View mDownView;
private boolean mPaused;
private float mFinalDelta;
/**
* Constructs a new swipe touch listener for the given @link android.support.v7.widget.RecyclerView
*
* @param recyclerView The recycler view whose items should be dismissable by swiping.
* @param listener The listener for the swipe events.
*/
public SwipeableRecyclerViewTouchListener(RecyclerView recyclerView, SwipeListener listener)
ViewConfiguration vc = ViewConfiguration.get(recyclerView.getContext());
mSlop = vc.getScaledTouchSlop();
mMinFlingVelocity = vc.getScaledMinimumFlingVelocity() * 16;
mMaxFlingVelocity = vc.getScaledMaximumFlingVelocity();
mAnimationTime = recyclerView.getContext().getResources().getInteger(
android.R.integer.config_shortAnimTime);
mRecyclerView = recyclerView;
mSwipeListener = listener;
/**
* This will ensure that this SwipeableRecyclerViewTouchListener is paused during list view scrolling.
* If a scroll listener is already assigned, the caller should still pass scroll changes through
* to this listener.
*/
mRecyclerView.setOnScrollListener(new RecyclerView.OnScrollListener()
@Override
public void onScrollStateChanged(RecyclerView recyclerView, int newState)
setEnabled(newState != RecyclerView.SCROLL_STATE_DRAGGING);
@Override
public void onScrolled(RecyclerView recyclerView, int dx, int dy)
);
/**
* 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;
@Override
public boolean onInterceptTouchEvent(RecyclerView rv, MotionEvent motionEvent)
return handleTouchEvent(motionEvent);
@Override
public void onTouchEvent(RecyclerView rv, MotionEvent motionEvent)
handleTouchEvent(motionEvent);
private boolean handleTouchEvent(MotionEvent motionEvent)
if (mViewWidth < 2)
mViewWidth = mRecyclerView.getWidth();
switch (motionEvent.getActionMasked())
case MotionEvent.ACTION_DOWN:
if (mPaused)
break;
// Find the child view that was touched (perform a hit test)
Rect rect = new Rect();
int childCount = mRecyclerView.getChildCount();
int[] listViewCoords = new int[2];
mRecyclerView.getLocationOnScreen(listViewCoords);
int x = (int) motionEvent.getRawX() - listViewCoords[0];
int y = (int) motionEvent.getRawY() - listViewCoords[1];
View child;
for (int i = 0; i < childCount; i++)
child = mRecyclerView.getChildAt(i);
child.getHitRect(rect);
if (rect.contains(x, y))
mDownView = child;
break;
if (mDownView != null)
mDownX = motionEvent.getRawX();
mDownY = motionEvent.getRawY();
mDownPosition = mRecyclerView.getChildPosition(mDownView);
if (mSwipeListener.canSwipe(mDownPosition))
mVelocityTracker = VelocityTracker.obtain();
mVelocityTracker.addMovement(motionEvent);
else
mDownView = null;
break;
case MotionEvent.ACTION_CANCEL:
if (mVelocityTracker == null)
break;
if (mDownView != null && mSwiping)
// cancel
mDownView.animate()
.translationX(0)
.alpha(1)
.setDuration(mAnimationTime)
.setListener(null);
mVelocityTracker.recycle();
mVelocityTracker = null;
mDownX = 0;
mDownY = 0;
mDownView = null;
mDownPosition = ListView.INVALID_POSITION;
mSwiping = false;
break;
case MotionEvent.ACTION_UP:
if (mVelocityTracker == null)
break;
mFinalDelta = 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(mFinalDelta) > mViewWidth / 2 && mSwiping)
dismiss = true;
dismissRight = mFinalDelta > 0;
else if (mMinFlingVelocity <= absVelocityX && absVelocityX <= mMaxFlingVelocity
&& absVelocityY < absVelocityX && mSwiping)
// dismiss only if flinging in the same direction as dragging
dismiss = (velocityX < 0) == (mFinalDelta < 0);
dismissRight = mVelocityTracker.getXVelocity() > 0;
if (dismiss && mDownPosition != ListView.INVALID_POSITION)
// 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;
mDownY = 0;
mDownView = null;
mDownPosition = ListView.INVALID_POSITION;
mSwiping = false;
break;
case MotionEvent.ACTION_MOVE:
if (mVelocityTracker == null || mPaused)
break;
mVelocityTracker.addMovement(motionEvent);
float deltaX = motionEvent.getRawX() - mDownX;
float deltaY = motionEvent.getRawY() - mDownY;
if (!mSwiping && Math.abs(deltaX) > mSlop && Math.abs(deltaY) < Math.abs(deltaX) / 2)
mSwiping = true;
mSwipingSlop = (deltaX > 0 ? mSlop : -mSlop);
if (mSwiping)
mDownView.setTranslationX(deltaX - mSwipingSlop);
mDownView.setAlpha(Math.max(0f, Math.min(1f,
1f - Math.abs(deltaX) / mViewWidth)));
return true;
break;
return false;
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;
if (mFinalDelta > 0)
mSwipeListener.onDismissedBySwipeRight(mRecyclerView, dismissPositions);
else
mSwipeListener.onDismissedBySwipeLeft(mRecyclerView, dismissPositions);
// Reset mDownPosition to avoid MotionEvent.ACTION_UP trying to start a dismiss
// animation with a stale position
mDownPosition = ListView.INVALID_POSITION;
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);
// Send a cancel event
long time = SystemClock.uptimeMillis();
MotionEvent cancelEvent = MotionEvent.obtain(time, time,
MotionEvent.ACTION_CANCEL, 0, 0, 0);
mRecyclerView.dispatchTouchEvent(cancelEvent);
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();
/**
* The callback interface used by @link SwipeableRecyclerViewTouchListener to inform its client
* about a swipe of one or more list item positions.
*/
public interface SwipeListener
/**
* Called to determine whether the given position can be swiped.
*/
boolean canSwipe(int position);
/**
* Called when the item has been dismissed by swiping to the left.
*
* @param recyclerView The originating @link android.support.v7.widget.RecyclerView.
* @param reverseSortedPositions An array of positions to dismiss, sorted in descending
* order for convenience.
*/
void onDismissedBySwipeLeft(RecyclerView recyclerView, int[] reverseSortedPositions);
/**
* Called when the item has been dismissed by swiping to the right.
*
* @param recyclerView The originating @link android.support.v7.widget.RecyclerView.
* @param reverseSortedPositions An array of positions to dismiss, sorted in descending
* order for convenience.
*/
void onDismissedBySwipeRight(RecyclerView recyclerView, int[] reverseSortedPositions);
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(@NonNull PendingDismissData other)
// Sort by descending position
return other.position - position;
activity_main.xml
<android.support.v7.widget.RecyclerView xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/recycler_view"
android:layout_
android:layout_
android:scrollbars="vertical">
</android.support.v7.widget.RecyclerView>
card_view_layout.xml
<?xml version="1.0" encoding="utf-8"?>
<android.support.v7.widget.CardView xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:card_view="http://schemas.android.com/apk/res-auto"
android:layout_
android:layout_
android:layout_margin="5dp"
android:clickable="true"
android:foreground="?android:attr/selectableItemBackground"
android:orientation="vertical"
card_view:cardCornerRadius="5dp">
<RelativeLayout
android:layout_
android:layout_>
<TextView
android:id="@+id/card_view_title"
android:layout_
android:layout_
android:layout_alignParentTop="true"
android:layout_centerHorizontal="true"
android:gravity="center"
android:textColor="@android:color/black"
android:textSize="24sp" />
<LinearLayout
android:layout_
android:layout_
android:layout_alignParentBottom="true"
android:layout_below="@id/card_view_title"
android:layout_centerHorizontal="true"
android:gravity="center">
<Button
android:id="@+id/card_view_button1"
android:layout_
android:layout_
android:text="Button1" />
<Button
android:id="@+id/card_view_button2"
android:layout_
android:layout_
android:text="Button2" />
</LinearLayout>
</RelativeLayout>
</android.support.v7.widget.CardView>
【问题讨论】:
我也在尝试使用 RecyclerView 和 CardviewAdapter 实现相同的功能,但无法找到确切的解决方案。如果您找到一些解决方案或下一步如何进行的方法,请与我们分享。 【参考方案1】:仅供参考,您可以使用这些 swipe layout 库。它包括您正在搜索的滑动操作。希望对你有帮助
【讨论】:
【参考方案2】:我刚刚完成了这个。看起来是这样的:
我没有CardView
,但你可以有任何布局(所以你可以添加支持库CardView
或任何你喜欢的东西)。
我就是这样做的。我用this library。由于库有 minSdkVersion
15 而我们有 14 个,我直接从 GitHub 复制粘贴了以下类:OnItemClickListener
、RecyclerViewAdapter
、SwipeableItemClickListener
、SwipeToDismissTouchListener
和 ViewAdapter
(这是您唯一的如果您使用RecyclerView
,则需要)。
由于lib还没有更新,需要在SwipeableItemClickListener
中添加如下方法:
@Override
public void onRequestDisallowInterceptTouchEvent(boolean disallowIntercept)
您的列表项布局应具有以下结构:
<FrameLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_
android:layout_
>
<!-- This is the layout you see always -->
<include
layout="@layout/list_item_article"
tools:visibility="gone"
/>
<!-- This is the layout shown after swiping. Must have visibility="gone" -->
<LinearLayout
android:visibility="gone"
tools:visibility="visible"
android:layout_
android:layout_
android:orientation="horizontal"
>
<TextView
android:id="@+id/swipe_cancel"
android:layout_
android:layout_
android:layout_weight="2"
android:text="@string/swipe_cancel"
android:gravity="center"
/>
<TextView
android:id="@+id/swipe_delete"
android:layout_
android:layout_
android:layout_weight="1"
android:text="@string/swipe_delete"
android:gravity="center"
android:background="#f00"
android:textColor="#fff"
/>
</LinearLayout>
</FrameLayout>
然后在创建RecyclerView
并设置它的适配器后,只需将此代码添加到您的Activity/Fragment
:
final SwipeToDismissTouchListener<RecyclerViewAdapter> touchListener =
new SwipeToDismissTouchListener<>(
new RecyclerViewAdapter(mRecyclerView),
new SwipeToDismissTouchListener.DismissCallbacks<RecyclerViewAdapter>()
@Override
public boolean canDismiss(int position)
return true;
@Override
public void onDismiss(RecyclerViewAdapter recyclerView, int position)
// remove item at position and notify adapter
);
mRecyclerView.setOnTouchListener(touchListener);
mRecyclerView.addOnScrollListener((RecyclerView.OnScrollListener) touchListener.makeScrollListener());
mRecyclerView.addOnItemTouchListener(new SwipeableItemClickListener(
getActivity(),
new OnItemClickListener()
@Override
public void onItemClick(View view, int position)
if (view.getId() == R.id.swipe_delete)
touchListener.processPendingDismisses();
else
touchListener.undoPendingDismiss();
));
【讨论】:
【参考方案3】:如果您只想拥有添加和删除按钮,您可以使用 RecyclerView,但使用 RecyclerView.Adapter 而不是 CardViewAdapter。 这是一个例子:
public class MyActivity extends Activity
private RecyclerView mRecyclerView;
private RecyclerView.Adapter mAdapter;
private RecyclerView.LayoutManager mLayoutManager;
@Override
protected void onCreate(Bundle savedInstanceState)
super.onCreate(savedInstanceState);
setContentView(R.layout.my_activity);
mRecyclerView = (RecyclerView) findViewById(R.id.my_recycler_view);
// use this setting to improve performance if you know that changes
// in content do not change the layout size of the RecyclerView
mRecyclerView.setHasFixedSize(true);
// use a linear layout manager
mLayoutManager = new LinearLayoutManager(this);
mRecyclerView.setLayoutManager(mLayoutManager);
// specify an adapter (see also next example)
mAdapter = new MyAdapter(myDataset);
mRecyclerView.setAdapter(mAdapter);
...
public class MyAdapter extends RecyclerView.Adapter<MyAdapter.ViewHolder>
private String[] mDataset;
// Provide a reference to the views for each data item
// Complex data items may need more than one view per item, and
// you provide access to all the views for a data item in a view holder
public static class ViewHolder extends RecyclerView.ViewHolder
// each data item is just a string in this case
public TextView mTextView;
public ViewHolder(TextView v)
super(v);
mTextView = v;
// Provide a suitable constructor (depends on the kind of dataset)
public MyAdapter(String[] myDataset)
mDataset = myDataset;
// Create new views (invoked by the layout manager)
@Override
public MyAdapter.ViewHolder onCreateViewHolder(ViewGroup parent,
int viewType)
// create a new view
View v = LayoutInflater.from(parent.getContext())
.inflate(R.layout.my_text_view, parent, false);
// set the view's size, margins, paddings and layout parameters
...
ViewHolder vh = new ViewHolder(v);
return vh;
// Replace the contents of a view (invoked by the layout manager)
@Override
public void onBindViewHolder(ViewHolder holder, int position)
// - get element from your dataset at this position
// - replace the contents of the view with that element
holder.mTextView.setText(mDataset[position]);
// Return the size of your dataset (invoked by the layout manager)
@Override
public int getItemCount()
return mDataset.length;
【讨论】:
以上是关于如何使我的 Android Swipeable CardView 更像 IOS 7 邮件应用程序(滑动以显示按钮)的主要内容,如果未能解决你的问题,请参考以下文章
如何在 UIView Like Swipeable TableView Cell 中制作类似于“滑动解锁”的动画
如何通过覆盖 BackButton 使我的 PhoneGap android 应用程序 onPause?