Talking about the historical origin of this blog, it is estimated that it has a "history". The historical origin of this blog, it is estimated that it has a "history". The earliest story can be traced back to when I tried the Tantan APP. When I first entered the software interface, I was attracted by the design of selecting "like/dislike" by sliding cards. At that time, I really wanted to achieve this Tantan-like effect by myself, but I had no idea. However, there is no doubt that the principle of this effect must be similar to ListView/RecyclerView, involving the recycling and reuse of Item View, otherwise it would have been OOM due to a large number of Item Views. Later, I saw many great bloggers also launched the same Tantan-like effect blogs. After reading them from beginning to end, I found that they were easy to understand and basically had no problems. So, the idea of achieving the Tantan-like effect came to my mind again. So, what are you still hesitating about? Let's do it while the heat is hot! I happily decided. The first problem we face is the consideration of implementing View. RecyclerView is the best choice! RecyclerView is the best choice! RecyclerView is the best choice! Important things should be said three times!!! The reason is, first, RecyclerView has its own Item View recycling and reuse function, so we don't need to consider this problem; second, the layout of RecyclerView is achieved by setting LayoutManager, which fully "decouples" the layout from RecyclerView. LayoutManager can be implemented in a customized way. This is exactly what we want!!! Another point, this is also one of the reasons why ListView is not selected. Now, let's get started and show you the moment of miracle. CardLayoutManager Create CardLayoutManager and inherit from RecyclerView.LayoutManager. We need to implement the generateDefaultLayoutParams() method ourselves: - @Override
-
- public RecyclerView.LayoutParams generateDefaultLayoutParams() {
-
- return new RecyclerView.LayoutParams(ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT);
-
- }
In general, you can write it like above. The following method is our focus. The onLayoutChildren(final RecyclerView.Recycler recycler, RecyclerView.State state) method is used to implement the Item View layout: - @Override
-
- public void onLayoutChildren(final RecyclerView.Recycler recycler, RecyclerView.State state) {
-
- super.onLayoutChildren(recycler, state);
-
- // Remove all views first
-
- removeAllViews();
-
- // Before layout, detach all child Views and put them into the Scrap cache
-
- detachAndScrapAttachedViews(recycler);
-
- int itemCount = getItemCount();
-
- // Here, we configure CardConfig.DEFAULT_SHOW_ITEM = 3 by default. That is, the number of cards displayed on the screen is 3
-
- // When the number of data sources is greater than the *** display number
-
- if (itemCount > CardConfig.DEFAULT_SHOW_ITEM) {
-
- // Loop the data source backwards so that the 0th data is at the top of the screen
-
- for ( int position = CardConfig.DEFAULT_SHOW_ITEM; position >= 0; position
-
- final View view = recycler.getViewForPosition(position);
-
- // Add Item View to RecyclerView
-
- addView( view );
-
- // Measure the Item View
-
- measureChildWithMargins( view , 0, 0);
-
- // getDecoratedMeasuredWidth( view ) can get the width of Item View
-
- // So widthSpace is the remaining value except Item View
-
- int widthSpace = getWidth() - getDecoratedMeasuredWidth( view );
-
- // Same reason
-
- int heightSpace = getHeight() - getDecoratedMeasuredHeight( view );
-
- // Place the Item View into the RecyclerView layout
-
- // The default layout here is placed in the center of the RecyclerView
-
- layoutDecoratedWithMargins( view , widthSpace / 2, heightSpace / 2,
-
- widthSpace / 2 + getDecoratedMeasuredWidth( view ),
-
- heightSpace / 2 + getDecoratedMeasuredHeight( view ));
-
- // Actually there are four cards on the screen, but we overlap the third and fourth cards so that it looks like there are only three cards
-
- // The fourth card is mainly to maintain the continuity of the animation
-
- if (position == CardConfig.DEFAULT_SHOW_ITEM) {
-
- // Scale according to certain rules and offset the Y axis.
-
- // CardConfig.DEFAULT_SCALE defaults to 0.1f, CardConfig.DEFAULT_TRANSLATE_Y defaults to 14
-
- view .setScaleX(1 - (position - 1) * CardConfig.DEFAULT_SCALE);
-
- view .setScaleY(1 - (position - 1) * CardConfig.DEFAULT_SCALE);
-
- view .setTranslationY((position - 1) * view .getMeasuredHeight() / CardConfig.DEFAULT_TRANSLATE_Y);
-
- } else if (position > 0) {
-
- view .setScaleX(1 - position * CardConfig.DEFAULT_SCALE);
-
- view .setScaleY(1 - position * CardConfig.DEFAULT_SCALE);
-
- view .setTranslationY(position * view .getMeasuredHeight() / CardConfig.DEFAULT_TRANSLATE_Y);
-
- } else {
-
- // The purpose of setting mTouchListener is that we want the top card to be able to slide freely
-
- // The cards on the second, third, etc. layers are not allowed to slide
-
- view .setOnTouchListener(mOnTouchListener);
-
- }
-
- }
-
- } else {
-
- // When the number of data sources is less than or equal to the maximum number of displays, it is similar to the above code
-
- for ( int position = itemCount - 1; position >= 0; position
-
- final View view = recycler.getViewForPosition(position);
-
- addView( view );
-
- measureChildWithMargins( view , 0, 0);
-
- int widthSpace = getWidth() - getDecoratedMeasuredWidth( view );
-
- int heightSpace = getHeight() - getDecoratedMeasuredHeight( view );
-
-
- layoutDecoratedWithMargins( view , widthSpace / 2, heightSpace / 2,
-
- widthSpace / 2 + getDecoratedMeasuredWidth( view ),
-
- heightSpace / 2 + getDecoratedMeasuredHeight( view ));
-
-
- if (position > 0) {
-
- view .setScaleX(1 - position * CardConfig.DEFAULT_SCALE);
-
- view .setScaleY(1 - position * CardConfig.DEFAULT_SCALE);
-
- view .setTranslationY(position * view .getMeasuredHeight() / CardConfig.DEFAULT_TRANSLATE_Y);
-
- } else {
-
- view .setOnTouchListener(mOnTouchListener);
-
- }
-
- }
-
- }
-
- }
-
-
- private View .OnTouchListener mOnTouchListener = new View .OnTouchListener() {
-
-
- @Override
-
- public boolean onTouch( View v, MotionEvent event) {
-
- RecyclerView.ViewHolder childViewHolder = mRecyclerView.getChildViewHolder(v);
-
- // Hand over the touch event to mItemTouchHelper to handle the card sliding event
-
- if (MotionEventCompat.getActionMasked(event) == MotionEvent.ACTION_DOWN) {
-
- mItemTouchHelper.startSwipe(childViewHolder);
-
- }
-
- return false ;
-
- }
-
- };
In general, CardLayoutManager is mainly used to lay out the Item View, and then make corresponding deviations according to the position. Let's take a look at the finished effect diagram: As you can see, the general effect is already there. What is missing is handling the touch sliding event. OnSwipeListener Before looking at the code for the sliding event, let's first define a listener. It is mainly used to listen to the card sliding event. The code is as follows, and the comments are also given. I think you can understand it: - public interface OnSwipeListener<T> {
-
-
- /**
-
- * Callback when the card is still sliding
-
- *
-
- * @param viewHolder The viewHolder of the sliding card
-
- * @param ratio The ratio of sliding progress
-
- * @param direction The direction of card sliding, CardConfig.SWIPING_LEFT means sliding to the left, CardConfig.SWIPING_RIGHT means sliding to the right,
-
- * CardConfig.SWIPING_NONE means neither left nor right
-
- */
-
- void onSwiping(RecyclerView.ViewHolder viewHolder, float ratio, int direction);
-
-
- /**
-
- * Callback when the card is completely slid out
-
- *
-
- * @param viewHolder The viewHolder of the slide-out card
-
- * @param t The data of the slide-out card
-
- * @param direction The direction in which the card slides out. CardConfig.SWIPED_LEFT means sliding out from the left; CardConfig.SWIPED_RIGHT means sliding out from the right
-
- */
-
- void onSwiped(RecyclerView.ViewHolder viewHolder, T t, int direction);
-
-
- /**
-
- * Callback when all cards slide out
-
- */
-
- void onSwipedClear();
-
-
- }
CardItemTouchHelperCallback Now, we can go back and look at card sliding. You must be familiar with ItemTouchHelper to handle the touch sliding events of Item View! We will call it CardItemTouchHelperCallback for now. For ItemTouchHelper.Callback, you need to configure swipeFlags and dragFlags in the getMovementFlags(RecyclerView recyclerView, RecyclerView.ViewHolder viewHolder) method. The specific method is as follows. For swipeFlags, we only care about the left and right directions: - @Override
-
- public int getMovementFlags(RecyclerView recyclerView, RecyclerView.ViewHolder viewHolder) {
-
- int dragFlags = 0;
-
- int swipeFlags = 0;
-
- RecyclerView.LayoutManager layoutManager = recyclerView.getLayoutManager();
-
- if (layoutManager instanceof CardLayoutManager) {
-
- swipeFlags = ItemTouchHelper.LEFT | ItemTouchHelper.RIGHT ;
-
- }
-
- return makeMovementFlags(dragFlags, swipeFlags);
-
- }
There is one more thing to note. As mentioned earlier, in order to prevent the second and third layers of cards from sliding, we need to set isItemViewSwipeEnabled() to return false. - @Override
-
- public boolean isItemViewSwipeEnabled() {
-
- return false ;
-
- }
Next, we need to rewrite the onMove(RecyclerView recyclerView, RecyclerView.ViewHolder viewHolder, RecyclerView.ViewHolder target) and onSwiped(RecyclerView.ViewHolder viewHolder, int direction) methods. However, because we configured dragFlags to 0 above, we can simply return false in onMove(RecyclerView recyclerView, RecyclerView.ViewHolder viewHolder, RecyclerView.ViewHolder target). - @Override
-
- public boolean onMove(RecyclerView recyclerView, RecyclerView.ViewHolder viewHolder, RecyclerView.ViewHolder target) {
-
- return false ;
-
- }
In this way, we turn our attention to the onSwiped(RecyclerView.ViewHolder viewHolder, int direction) method: - @Override
-
- public void onSwiped(RecyclerView.ViewHolder viewHolder, int direction) {
-
- // Remove the previously set onTouchListener, otherwise the touch sliding will be messed up
-
- viewHolder.itemView.setOnTouchListener( null );
-
- // Delete the corresponding data
-
- int layoutPosition = viewHolder.getLayoutPosition();
-
- T remove = dataList.remove(layoutPosition);
-
- adapter.notifyDataSetChanged();
-
- // Callback OnSwipeListener listener after the card slides out
-
- if (mListener != null ) {
-
- mListener.onSwiped(viewHolder, remove, direction == ItemTouchHelper. LEFT ? CardConfig.SWIPED_LEFT : CardConfig.SWIPED_RIGHT);
-
- }
-
- // Callback OnSwipeListener listener when there is no data
-
- if (adapter.getItemCount() == 0) {
-
- if (mListener != null ) {
-
- mListener.onSwipedClear();
-
- }
-
- }
-
- }
After writing, let's take a look at the sliding effect: We found that something is missing. Yes, animation is missing. During the sliding process, we can override the onChildDraw(Canvas c, RecyclerView recyclerView, RecyclerView.ViewHolder viewHolder, float dX, float dY, int actionState, boolean isCurrentlyActive) method to add animation: - @Override
-
- public void onChildDraw(Canvas c, RecyclerView recyclerView, RecyclerView.ViewHolder viewHolder,
-
- float dX, float dY, int actionState, boolean isCurrentlyActive) {
-
- super.onChildDraw(c, recyclerView, viewHolder, dX, dY, actionState, isCurrentlyActive);
-
- View itemView = viewHolder.itemView;
-
- if (actionState == ItemTouchHelper.ACTION_STATE_SWIPE) {
-
- // Get the sliding threshold
-
- float ratio = dX / getThreshold(recyclerView, viewHolder);
-
- // ratio is 1 or -1
-
- if (ratio > 1) {
-
- ratio = 1;
-
- } else if (ratio < -1) {
-
- ratio = -1;
-
- }
-
- // The default rotation angle is 15 degrees
-
- itemView.setRotation(ratio * CardConfig.DEFAULT_ROTATE_DEGREE);
-
- int childCount = recyclerView.getChildCount();
-
- // When the number of data sources is greater than the *** display number
-
- if (childCount > CardConfig.DEFAULT_SHOW_ITEM) {
-
- for ( int position = 1; position < childCount - 1; position++) {
-
- int index = childCount - position - 1;
-
- View view = recyclerView.getChildAt(position);
-
- // Same as onLayoutChildren, but with the opposite animation
-
- view .setScaleX(1 - index * CardConfig.DEFAULT_SCALE + Math. abs (ratio) * CardConfig.DEFAULT_SCALE);
-
- view .setScaleY(1 - index * CardConfig.DEFAULT_SCALE + Math. abs (ratio) * CardConfig.DEFAULT_SCALE);
-
- view .setTranslationY(( index - Math. abs (ratio)) * itemView.getMeasuredHeight() / CardConfig.DEFAULT_TRANSLATE_Y);
-
- }
-
- } else {
-
- // When the number of data sources is less than or equal to the *** display number
-
- for ( int position = 0; position < childCount - 1; position++) {
-
- int index = childCount - position - 1;
-
- View view = recyclerView.getChildAt(position);
-
- view .setScaleX(1 - index * CardConfig.DEFAULT_SCALE + Math. abs (ratio) * CardConfig.DEFAULT_SCALE);
-
- view .setScaleY(1 - index * CardConfig.DEFAULT_SCALE + Math. abs (ratio) * CardConfig.DEFAULT_SCALE);
-
- view .setTranslationY(( index - Math. abs (ratio)) * itemView.getMeasuredHeight() / CardConfig.DEFAULT_TRANSLATE_Y);
-
- }
-
- }
-
- //Callback listener
-
- if (mListener != null ) {
-
- if (ratio != 0) {
-
- mListener.onSwiping(viewHolder, ratio, ratio < 0 ? CardConfig.SWIPING_LEFT : CardConfig.SWIPING_RIGHT);
-
- } else {
-
- mListener.onSwiping(viewHolder, ratio, CardConfig.SWIPING_NONE);
-
- }
-
- }
-
- }
-
- }
-
-
- private float getThreshold(RecyclerView recyclerView, RecyclerView.ViewHolder viewHolder) {
-
- return recyclerView.getWidth() * getSwipeThreshold(viewHolder);
-
- }
Now let's add animation and see the effect: I found that there is still a problem. After the cards in the first layer are slid out, the cards in the second layer are inexplicably tilted. This is because the Item View reuse mechanism is "playing tricks". So we should reset it in the clearView(RecyclerView recyclerView, RecyclerView.ViewHolder viewHolder) method: - @Override
-
- public void clearView(RecyclerView recyclerView, RecyclerView.ViewHolder viewHolder) {
-
- super.clearView(recyclerView, viewHolder);
-
- viewHolder.itemView.setRotation(0f);
-
- }
It's done, let's try the effect: Perfect! This is exactly what we were dreaming of. We finally made it happen!!! To sum up, in this whole code flow we mainly use custom LayoutManager and ItemTouchHelper.Callback. Overall it is quite simple, I believe you already know how to do it. |