I’ve been working on my final project. I need to implement a slide card effect in my final project. I took some time to implement it myself, using ItemTouchHelper and LayoutManager. Let’s take a look at the effect:

ViewPager
ViewPager
View
RecyclerView
RecyclerView

1. Effect analysis

Looking at the animation trouble, it’s actually very easy to implement it in two parts. First of all, each ItemView is presented in a superposition style. This effect does not exist in the common LayoutManger, so we need to define a LayoutManager to implement this style. That’s one thing. Second, how to achieve the effect of sliding switch? Remember we analyzed the ItemTouchHelper class earlier? The purpose of this class is to achieve the effect of swiping delete and drag, and here the effect of switching cards is the same as swiping delete, but the animation of the swiping is different. How does the ItemTouchHelper animate the card based on its finger swipe? The answer is in the onChildDraw method. In fact, we know from ItemTouchHelper’s onChildDraw method that the native is only doing the horizontal position change, so we can override this method to add the animation we want. In this way, isn’t this animation very simple? Next, let’s take a look at the code.

2. LayoutManager

Custom LayoutManager knowledge, I in RecyclerView source code analysis (seven) – custom LayoutManager and its related components of the source code analysis article has been explained in detail, here I will not repeat. Let’s go straight to the code. The key code is in the onLayoutChildren method:

    @Override
    public void onLayoutChildren(RecyclerView.Recycler recycler, RecyclerView.State state) {
        final int layoutCount = Math.min(getItemCount(), mMaxVisibleCount);
        detachAndScrapAttachedViews(recycler);
        for(int i = layoutCount - 1; i >= 0; i--) { final View view = recycler.getViewForPosition(i); 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)); // Set scale View.setScalex (() for each ItemView.float) Math.pow(DEFAULT_SCALE, i));
            view.setScaleY((float) Math.pow(DEFAULT_SCALE, i));
            if (i == 0) {
                view.setOnTouchListener(new View.OnTouchListener() {
                    @Override
                    public boolean onTouch(View v, MotionEvent event) {
                        RecyclerView.ViewHolder childViewHolder = mRecyclerView.getChildViewHolder(v);
                        if(event.getactionmasked () == motionEvent.action_down) {// You need to manually tell ItemTouchHelper that it can slide mItemTouchHelper.startSwipe(childViewHolder); }return false; }}); }else{// Set null View. setOnTouchListener(null); }}}Copy the code

I believe that the above code everyone can understand, here I will not explain line by line. But there is one thing we should pay special attention to:

        for(int i = layoutCount - 1; i >= 0; I --) {// ····Copy the code

In this case, we added the View backwards, which is an ItemView. The internal index of RecyclerView is 0, but in the Adapter, it is layoutCount -1. When we customize itemTouchHelper.callback, It’s gonna make a big difference.

3.ItemTouchHelper.Callback

ItemTouchHelper is a RecyclerView extension that RecyclerView and RecyclerView extension that RecyclerView and RecyclerView. Let’s go straight to the implementation code. The key is the onChildDraw method:

    @Override
    public void onChildDraw(Canvas c, RecyclerView recyclerView, RecyclerView.ViewHolder viewHolder, float dX, floatDY, int actionState, Boolean isCurrentlyActive) {super.onChilddraw (c, recyclerView, viewHolder, dX, dY, actionState, isCurrentlyActive); final View itemView = viewHolder.itemView;if (actionState == ItemTouchHelper.ACTION_STATE_SWIPE) {
            float ratio = dX / getThreshold(recyclerView, viewHolder);
            if (ratio > 1) {
                ratio = 1;
            } else if(ratio < -1) { ratio = -1; } // Rotate itemView.setrotation (ratio * 15);for(int i = 0; i < mMaxVisibleCount - 1; Child = recyclerView.getChildAt(I); finalfloat currentScale = (float) Math.pow(DEFAULT_SCALE, 2 - i);
                final float nextScale = currentScale / DEFAULT_SCALE;
                final floatscale = (nextScale - currentScale); child.setScaleX(Math.min(1, currentScale + scale * Math.abs(ratio))); child.setScaleY(Math.min(1, currentScale + scale * Math.abs(ratio))); }}}Copy the code

I have explained the above code in the comments, but I will not explain it here. But here’s one more thing:

            for(int i = 0; i < mMaxVisibleCount - 1; I++) {// ····}Copy the code

Here I scale 0 ~ mmaxVisiblecount-1 ItemView. Remember, this is not the position of the ItemView in Adapter, but the index of the ItemView in RecyclerView. As I explained earlier in LayoutManager, these two are reversed. So this should be 0 to mMaxVisibleCount minus 1. The whole implementation is so simple, in fact, there are still pits, such as:

    @Override
    public void clearView(RecyclerView recyclerView, RecyclerView.ViewHolder viewHolder) {
        super.clearView(recyclerView, viewHolder);
        viewHolder.itemView.setRotation(0f);
    }
Copy the code

You have to reset it in the clearView method, because ItemView is reusable, and not resetting it will cause problems. For example, the isItemViewSwipeEnabled method must be overridden (it’s ok not to, but the official documentation recommends it):

    @Override
    public boolean isItemViewSwipeEnabled() {
        return false;
    }
Copy the code

4. Conflicts with the SwipeRefreshLayout event

After using the code above, we can see that the RecyclerView inside SwipeRefreshLayout will cause event conflicts. Let me briefly describe the event collision situation: when we swipe left and right, this is normal, and each ItemView is a normal side flip; But when swiping up and down, normally SwipeRefreshLayout is swiped, but actually ItemView is swiped. As for the solution, I have two solutions: 1. Rewrite the onInterceptTouchEvent method of SwipeRefreshLayout to intercept events so that events cannot be passed to ItemView; 2. Cancel calling ItemTouchHelper’s startSwipe method manually and let ItemTouchHelper determine if it meets the conditions for sideswipe. Here, I specifically illustrate the first method. Why specify the first method? Because this method has big problems: 1. Rewrites SwipeRefreshLayout, which causes unnecessary work, for one thing; 2. Overwriting SwipeRefreshLayout will break the structure of SwipeRefreshLayout, which is the biggest drawback. Why would rewriting SwipeRefreshLayout break its structure? SwipeRefreshLayout does not actively block events because SwipeRefreshLayout uses nested sliding mechanisms. If we use onInterceptTouchEvent, we are violating SwipeRefreshLayout. Therefore, the first method is especially not recommended!! Second, let’s look at the implementation of the second solution, which is very simple and boils down to two sentences:

  1. inCallbackDon’t rewrite it insideisItemViewSwipeEnabledMethod,
  2. inLayoutManagerInside not in eachItemViewtheOnTouchListenerIt callsItemTouchHelperthestartSwipeMethods.

I’m going to explain briefly here why the second way doesn’t conflict, but to understand why it doesn’t conflict, you have to understand why it did conflict in the past. SwipeRefreshLayout itself does not block events, so all events can be passed to each ItemView in RecyclerView. Since we call the ItemTouchHelper startSwipe on OnTouchListener to indicate that an ItemView is selected and can be sideswiped, subsequent events will be consumed by that ItemView, resulting in event collisions. Instead, uncall the startSwipe method and let ItemTouchHelper select an ItemView that can slide. The ItemTouchHelper itself handles the conflict between slide up and slide left and right. The upslip of RecyclerView conflicts with the sideslippage of ItemView. That’s how the second approach works.

5. The source code

In order to facilitate your understanding, I uploaded my own Demo code to Github for your reference: SlideCardDemo