自定义 viewgroup+viewdraghelper 仿探探卡片式滑动

2,633 阅读6分钟
原文链接: blog.csdn.net

自定义viewGroup+ViewDragHelper: 仿探探主页卡片式滑动,层叠布局

前几天看到群里的dalao张旭童用recycleview写了一个这个效果但是我对自定义LayoutManager不熟,刚好又在学习自定义view,所以想到用自定义ViewGroup写写试试,不多说,先上效果图。
这里写图片描述
数据来自豆瓣的电影评分榜,从图上看到,我们可以滑动最顶层topview卡片,此时下面的卡片也随之变大,top-1view会变大到topview一致,下面的卡片以此类推,当topview右滑到一定距离时会删除,此时top-1view成了topview。
总的来说,分为以下几个小功能。

  • 拖拽顶层view(使用工具类ViewDragHelper推荐看翔哥的这篇)和角度旋转

  • 下面页面的放大和缩小

  • 滑动到一定程度后删除

不多说 先上代码

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

    public static int TRANS_Y_GAP;
    //卡片阶梯之间的宽度,单位px
    private int transY = 12;
    private ViewDragHelper mDragHelper;
    //最顶层页面,随着手指滑动
    private View topView;
    //卡片中心点
    private int centerX,centerY;
    //手指离开屏幕的判断
    private boolean isRelise;
    //加载数据的adapter
    private CardBaseAdapter adapter;
    //可见的卡片页面
    private int showCards = 3;
    //随手指滑动 卡片旋转的角度
    private int ROTATION = 20;
    //左滑右滑判断
    private boolean swipeLeft = false;
    //已经删除的页面的数量
    private int deleteNum;
    //子view的行宽度,高度
    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);
                        //mAutoBackView手指释放时可以自动回去

                        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);
                        //当手指松开后对顶层卡片进行移动
                        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){
                                //根据角度来对卡片旋转角度进行测算
                                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);

        //子view
        View child = null;
        //获取子view的margin值
        MarginLayoutParams params = null;
            if (getChildCount()>0){
                child = getChildAt(0);
                //这里我就是用第一个页面的大小来当做长款,因为后面不可能比他大了
                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));
                //这里说明下,虽然你可见的是4张卡片,但是其实加载了5行,第五张和第四章重叠,这么做是为了滑动顶部view
               // 时第四张卡片滑动时可以显示,所以这里第四张和第五张卡片的位置是一样的。
                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不能为空");
        this.adapter = adapter;
        //初始化数据 你需要显示几个页面
        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);
            //后面加载进来数据都放在最底层
            addView(view,0);
        }

    }
    private void changeViews() {
        View view = null;
        /**
         *   showCards 是你需要显示几张卡片,showCards-j是为了排列顺序
         *   viewgroup是最先加进来的view是在最底层的,所以我为了让第一个加进来的放在最上层,用了这个
         *   举个栗子:显示3张页面 showCards = 3,先加载第四个页面(因为最底层还要有一个你看不到的页面)放在最底层,
         *   到最后j=3时 加载第一张页面数据,同时将它显示优先级设为最高addView(view,j);
         *   deleteNum是你右滑删掉的页面数量
          */
        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;
    }
}

下面对最重要的onlayout代码分析一下,其他滑动的算法和这个基本一致

  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));
                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));
        }
  • 卡片层叠间隔。如图showCards是可见卡片数量,TRANS_Y_GAP是下端露出的宽度,这里是对下面这一块进行计算,方便下面的layout。
  • 这里写图片描述这里我先将这些卡片layout为屏幕的中点,然后对他们进行缩放,代码中0.05f就是缩放比例,第一层缩放0.05,第二层0.10,第三层0.15,以此类推,上图中颜色标注的就是两个缩放区域,他们分别是0.05f的一半,后面代码中可以看到。
  • view.setTranslationY((childHeight*0.025f+TRANS_Y_GAP) * (level - 1));
  • 当layout好他们的位置之后就可以对他们进行位移。childHeight*0.025f位移的就是上图颜色块的距离然后再加上卡片间隔距离TRANS_Y_GAP再乘以他们的阶次,就完成整个的布局了。
  • 后面clampViewPositionHorizontal,onViewReleased和onViewPositionChanged方法的算法和这个类似。大致的注释在代码中已经写了,又不懂的可以留言我。
  • 先去学习下ViewDragHelper再来看比较好
  • 文中的adapter是我自己写的一个自定义adapter,这里我就不列了,想要的自己可以去下载。
  • 如果你对卡片大小不满意 ,可以自己去设置,就那个0.05f,这里忘了给他设置成全局变量了,点击事件我也没加了,你们有需要的可以自己加,实在不会评论我,我发给你,其实也不难。
  • 在这里我设了两个外部可以控制的变量,卡片可见数量和卡片间隔距离,在外部这样调用就可以了
  • swipeCards.setShowCards(5)
    .setTransY(50)
    .setAdapter(new CardBaseAdapter(this,subjectsList));

很久没写博客了,都不怎么会写了,也不怎么会去表达了。暂时就想到这么多,又是想法可以留言我,我一定会看的,恩 就这么多了。