Custom viewGroup+ViewDragHelper: simulate the home page card slide, cascading layout

Saw Dalao in the group the other dayZhang XutongUse RecycleView to write a this effect but I am not familiar with custom LayoutManager, just in learning custom view, so I thought of using custom ViewGroup to write try, not to say much, first effect diagram.



The data comes from douban’s movie rating list. As can be seen from the figure, we can slide the topview card on the top layer, and then the cards below will also become larger. Top-1view will become larger to be consistent with TopView. Now top-1View becomes TopView.

In general, it is divided into the following small functions.

  • Drag and drop the top-level view (using the utility class ViewDragHelperI recommend this article by Xiang Ge) and angular rotation

  • Zoom in and out of the following pages

  • Slide to a certain point and delete

So let’s go to the code first

public class SwipeCardView extends ViewGroup {
    private static final String TAG = "SwipeCardView";

    public static int TRANS_Y_GAP;
    // The width between the card steps, in px
    private int transY = 12;
    private ViewDragHelper mDragHelper;
    // Top page, swipe with your finger
    private View topView;
    // Card center point
    private int centerX,centerY;
    // Finger off screen judgment
    private boolean isRelise;
    // Load the adapter of the data
    private CardBaseAdapter adapter;
    // The visible card page
    private int showCards = 3;
    // Slide the rotation Angle of the card with your finger
    private int ROTATION = 20;
    // Swipe left and right to judge
    private boolean swipeLeft = false;
    // Number of pages that have been deleted
    private int deleteNum;
    // The row width and height of the subview
    int childWidth, childHeight;
    public SwipeCardView(Context context) {
        this(context, null);
    }


    public SwipeCardView(Context context, AttributeSet attrs) {
        this(context, attrs, 0);
    }

    public SwipeCardView(Context context, AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
        TRANS_Y_GAP = (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, transY, context.getResources().getDisplayMetrics());
        mDragHelper = ViewDragHelper.create(this.1.0f, new ViewDragHelper.Callback() {
                    @Override
                    public boolean tryCaptureView(View child, int pointerId) {
                        return child == topView;
                    }

                    @Override
                    public int clampViewPositionHorizontal(View changedView, int left, int dx) {

                        if (isRelise) {
                            isRelise = false;
                        }

                        for (int i = 1; i < getChildCount()-1; i++) {
                            View view = getChildAt(i);
                            view.setTranslationY((childHeight*0.025f+TRANS_Y_GAP) * ( getChildCount()-1- i)
                                                -getCenterX(changedView)*(childHeight*0.025f+TRANS_Y_GAP));
                            view.setScaleX(1-( getChildCount()-1-i)*0.05f + getCenterX(changedView) * 0.05f);
                            view.setScaleY(1-( getChildCount()-1-i)*0.05f + getCenterX(changedView) * 0.05f);
                        }
                        if(topView! =null) {if (swipeLeft){
                                topView.setRotation(-getCenterX(changedView) * ROTATION);
                            }else{ topView.setRotation(getCenterX(changedView) * ROTATION); }}return left;
                    }
                    @Override
                    public int clampViewPositionVertical(View child, int top, int dy) {
                        return top;
                    }

                    @Override
                    public void onViewReleased(View releasedChild, float xvel, float yvel) {
// super.onViewReleased(releasedChild, xvel, yvel);
                        // The mAutoBackView finger can automatically go back when released

                        if (releasedChild.getLeft() / 2 > 300) {

                            if (releasedChild == topView) {
                                removeView(topView);
                                deleteNum++;
                                for (int i = 1; i < getChildCount()-1; i++) {
                                    View view = getChildAt(i);
                                    int level =  getChildCount()-1-i;
                                    view.setTranslationY((childHeight*0.025f+TRANS_Y_GAP) * (level));
                                    view.setScaleX(1 - 0.05f * ( level));
                                    view.setScaleY(1 - 0.05f * ( level)); } adapter.notifyDataSetChanged(); }}else {

                            isRelise = true;
                            mDragHelper.settleCapturedViewAt((int) (centerX-childWidth/2), (int) (centerY-childHeight/2)); invalidate(); }}@Override
                    public void onViewPositionChanged(View changedView, int left, int top, int dx,
                                                      int dy) {
                        super.onViewPositionChanged(changedView, left, top, dx, dy);
                        // Move the top card when the finger is released
                        if (changedView == topView && isRelise) {

                            for (int i = 1; i < getChildCount()-1; i++) {
                                View view = getChildAt(i);
                                int level =  getChildCount()-1-i;
                                view.setTranslationY((childHeight*0.025f+TRANS_Y_GAP) * ( level)-
                                        getCenterX(changedView)*(childHeight*0.025f+TRANS_Y_GAP));
                                view.setScaleX(1-(level)*0.05f + getCenterX(changedView) * 0.05f);
                                view.setScaleY(1-(level)*0.05f + getCenterX(changedView) * 0.05f);
                            }
                            if(topView! =null) {// Measure the rotation Angle of the card according to the Angle
                                if (swipeLeft){
                                    topView.setRotation(-getCenterX(changedView) * ROTATION);
                                }else{ topView.setRotation(getCenterX(changedView) * ROTATION); }}}}}); mDragHelper.setEdgeTrackingEnabled(ViewDragHelper.EDGE_LEFT); }private float getCenterX(View child) {
        if (child.getWidth() / 2 + child.getX() - centerX<0){
            swipeLeft = true;
        }else {
            swipeLeft = false;
        }
        float width = Math.abs(child.getWidth() / 2 + child.getX() - centerX);
        if (width > centerX) {
            width = centerX;
        }
        return width / centerX;
    }


    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        super.onMeasure(widthMeasureSpec, heightMeasureSpec);
        int widthMode = MeasureSpec.getMode(widthMeasureSpec);
        int widthSize = MeasureSpec.getSize(widthMeasureSpec);
        int heightMode = MeasureSpec.getMode(heightMeasureSpec);
        int heightSize = MeasureSpec.getSize(heightMeasureSpec);

        centerX = widthSize / 2;
        centerY = heightSize/2;
        measureChildren( widthMeasureSpec, heightMeasureSpec);

        / / the view
        View child = null;
        // Get the margin of the child view
        MarginLayoutParams params = null;
            if (getChildCount()>0){
                child = getChildAt(0);
                // I just use the size of the first page as the length, because it can't be larger than it
                measureChild(child, widthMeasureSpec, heightMeasureSpec);
                params = (MarginLayoutParams) child.getLayoutParams();
                childWidth = child.getMeasuredWidth() + params.leftMargin + params.rightMargin;
                childHeight = child.getMeasuredHeight() + params.topMargin + params.bottomMargin;
            }

        setMeasuredDimension(widthSize, heightSize);
    }

    @Override
    public void computeScroll() {
        if (mDragHelper.continueSettling(true)) { invalidate(); }}@Override
    protected void onLayout(boolean b, int i, int i1, int i2, int i3) {
        topView = getChildAt(getChildCount()-1);

        int level = getChildCount()  - 1;
        View view;

        if (getChildCount() > 1) {
            for (int j = 0; j<=getChildCount() -1; j++) {
                view = getChildAt(j);

                view.layout((int) (centerX-childWidth/2), (int) (centerY-childHeight/2),
                        (int) (centerX+childWidth/2), (int) (centerY+childHeight/2));
                view.setTranslationY((childHeight*0.025f+TRANS_Y_GAP) * (level - 1));
                view.setScaleX(1 - 0.05f * (level - 1));
                view.setScaleY(1 - 0.05f * (level - 1));
                // For clarification, although you can see 4 cards, 5 rows are loaded, and chapter 5 overlaps with chapter 4 in order to slide the top view
               // The fourth card can be displayed when the fourth card slides, so the position of the fourth and fifth cards is the same here.
                if(j! =0){ level--; }}}else    if (getChildCount() > 0) {
            view = getChildAt(0);
            view.layout((int) (centerX-childWidth/2), (int) (centerY-childHeight/2),
                    (int) (centerX+childWidth/2), (int) (centerY+childHeight/2)); }}public void setAdapter(@NonNull CardBaseAdapter adapter) {
        if (adapter == null) throw new NullPointerException("Adapter cannot be empty");
        this.adapter = adapter;
        You need to display several pages to initialize the data
        changeViews();
        adapter.registerDataSetObserver(new DataSetObserver() {

            @Override
            public void onChanged() {
                getMore();
            }

            @Override
            public void onInvalidated() { getMore(); }}); }public void getMore() {
        if (getChildCount()+deleteNum<adapter.getCount()){
            View view = adapter.getView(getChildCount()+deleteNum,
                    getChildAt(getChildCount()),this);
            // All data is placed at the bottom
            addView(view,0); }}private void changeViews() {
        View view = null;
        /** * showCards-j is the order in which you want to display the cards. ** viewgroup is the first view to be added. AddView (view,j); addView(view,j); showCards =3; addView(view,j); * deleteNum is the number of pages you right-click to delete */
        for (int j = 0; j <=showCards; j++) {
            if (j+deleteNum<adapter.getCount()){
                view = adapter.getView(showCards-j, getChildAt(j),this); addView(view,j); }}}@Override
    public boolean onInterceptHoverEvent(MotionEvent event) {
        return mDragHelper.shouldInterceptTouchEvent(event);
    }

    @Override
    public boolean onTouchEvent(MotionEvent event) {
        mDragHelper.processTouchEvent(event);
        return true;
    }


    @Override
    public ViewGroup.LayoutParams generateLayoutParams(AttributeSet attrs) {
        return new MarginLayoutParams(getContext(),attrs);
    }

    public SwipeCardView setShowCards(int showCards) {
        this.showCards = showCards;
        return this;
    }

    public SwipeCardView setTransY(int transY) {
        this.transY = transY;
        return this; }}Copy the code

Here is the most important onLayout code analysis, other sliding algorithms and this is basically the same

  topView = getChildAt(getChildCount()-1);

        int level = getChildCount()  - 1;
        View view;

        if (getChildCount() > 1) {
            for (int j = 0; j<=getChildCount() -1; j++) {
                view = getChildAt(j);

                view.layout((int) (centerX-childWidth/2), (int) (centerY-childHeight/2),
                        (int) (centerX+childWidth/2), (int) (centerY+childHeight/2));
                view.setTranslationY((childHeight*0.025 f+TRANS_Y_GAP) * (level - 1));
                view.setScaleX(1 - 0.05 f * (level - 1));
                view.setScaleY(1 - 0.05 f * (level - 1));
                if(j! =0){ level--; }}}else    if (getChildCount() > 0) {
            view = getChildAt(0);
            view.layout((int) (centerX-childWidth/2), (int) (centerY-childHeight/2),
                    (int) (centerX+childWidth/2), (int) (centerY+childHeight/2));
        }Copy the code
  • . As shown in the figure, showCards is the number of visible cards, and TRANS_Y_GAP is the width exposed at the bottom. Here is the calculation of the following piece to facilitate the layout below.
  • In the code, 0.05F is the scaling ratio. The first layer is scaled 0.05, the second layer is scaled 0.10, the third layer is scaled 0.15, and so on. In the figure above, the two color marked scaling areas are half of 0.05F respectively, which can be seen in the following code.
  • View. setTranslationY((childHeight*0.025f+TRANS_Y_GAP) * (level-1));
  • Once the layout has positioned them, you can move them. ChildHeight *0.025f moves the distance of the color blocks above and then adds TRANS_Y_GAP times their order to complete the layout.
  • Behind clampViewPositionHorizontal onViewReleased and onViewPositionChanged method of algorithm and the like. General comments have been written in the code, and do not understand the message I can.
  • It’s a good idea to learn about ViewDragHelper first
  • The adapter in the article is a customized adapter written by myself. I will not list it here. If you want, you can download it yourself.
  • If you are not satisfied with the size of the card, you can set the 0.05F by yourself. I forgot to set it as a global variable here, and I did not add the click event. If you need it, you can add it yourself.
  • Here I set up two externally controllable variables, the number of visible cards and the distance between cards, which can be called externally
  • swipeCards.setShowCards(5)

    .setTransY(50)

    .setAdapter(new CardBaseAdapter(this,subjectsList));

I haven’t written a blog for a long time. I don’t know how to write or express myself. Temporarily think of so much, and the idea can leave a message to me, I will see, en so much.