Android之自定View-最简单的可拖拽式层叠卡片

转载请标明出处:
http://blog.csdn.net/hai_qing_xu_kong/article/details/72935315
本文出自:【顾林海的博客】

前言

记得两年多前在同程旅游的时候,第一个周边游的项目要求做一款卡片类,可左右拖拽,当初实现的比较粗糙,而且实现方式也相对复杂,今天有空翻看之前写的卡片控件,突然有了更好的实现思路,下面看看实现后的效果:

这里写图片描述


使用说明

下面有github地址,下载下来后,把相关源码拷贝到自己的项目中接着在自己的layout文件中放入CardGroupView控件:

<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent">

    <com.glh.cardview.card.CardGroupView
        android:id="@+id/card"
        android:layout_width="match_parent"
        android:layout_height="match_parent" />
</RelativeLayout>

CardGroupView控件提供了以下方法:

	/**
     * 移除顶部卡片(无动画)
     */
    public void removeTopCard()

	 /**
     * 移除顶部卡片(有动画)
     *
     * @param left 向左吗
     */
    public void removeTopCard(boolean left)
    
	/**
     * 当剩余卡片等于size时,加载更多
     */
	public void setloadSize(int size)

	/**
     * 加载更多监听
     *
     * @param listener {@link LoadMore}
     */
    public void setLoadMoreListener(LoadMore listener)

	 /**
     * 左右滑动监听
     *
     * @param listener {@link LeftOrRight}
     */
    public void setLeftOrRightListener(LeftOrRight listener)

在Activity中设置CardGroupView并添加卡片:

package com.glh.cardview;

import android.support.v7.app.AppCompatActivity;
import android.os.Bundle;
import android.view.LayoutInflater;
import android.view.View;
import android.widget.Toast;

import com.glh.cardview.card.CardGroupView;

public class MainActivity extends AppCompatActivity {

    private CardGroupView mCardGroupView;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        initView();
        initEvent();
        addCard();
    }


    private void initView() {
        mCardGroupView = (CardGroupView) findViewById(R.id.card);
        mCardGroupView.setloadSize(3);
    }

    private void initEvent() {
        mCardGroupView.setLoadMoreListener(new CardGroupView.LoadMore() {
            @Override
            public void load() {
                mCardGroupView.addView(getCard());
                mCardGroupView.addView(getCard());
                mCardGroupView.addView(getCard());
                mCardGroupView.addView(getCard());
                mCardGroupView.addView(getCard());

            }
        });
        mCardGroupView.setLeftOrRightListener(new CardGroupView.LeftOrRight() {
            @Override
            public void leftOrRight(boolean left) {
                if (left) {
                    Toast.makeText(MainActivity.this, "向左滑喜欢!", Toast.LENGTH_SHORT).show();
                } else {
                    Toast.makeText(MainActivity.this, "向右滑不喜欢!", Toast.LENGTH_SHORT).show();
                }
            }
        });
    }

    private void addCard() {
        mCardGroupView.addView(getCard());
        mCardGroupView.addView(getCard());
        mCardGroupView.addView(getCard());
        mCardGroupView.addView(getCard());
        mCardGroupView.addView(getCard());

    }

    private View getCard() {
        View card = LayoutInflater.from(this).inflate(R.layout.layout_card, null);
        View view = card.findViewById(R.id.remove);
        view.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                mCardGroupView.removeTopCard(true);
            }
        });
        return card;
    }
}


实现原理

大家在实现自定义View时,千万不要想一口吃成胖子,一步一步的实现是最稳妥的,上面卡片显示的时候可以看出是层叠样式的,那怎样才能让卡片层叠呢?这里我想到的方式是:

1、用RelativeLayout作为卡片的容器,为什么呢?因为在RelativeLayout中,我们将所有的卡片居中对齐,这时所有卡片会以下列方式展示:

这里写图片描述

2、我们发现上图中的卡片全部层叠在一起了,但我们的效果图中底部是有层叠样式的,因此我们在将卡片添加到RelativeLayout时,设置它距离底部的位置:

这里写图片描述

代码实现:

 @Override
    public void addView(View card) {
		this.mCardList.add(card);
        LayoutParams layoutParams = new LayoutParams(LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT);
			        layoutParams.addRule(RelativeLayout.CENTER_VERTICAL);
        this.addView(card, 0, layoutParams);
        card.setOnTouchListener(onTouchListener);
        this.setLayoutParams(card, mCardList.size());
    }

/**
     * 设置卡片LayoutParams
     *
     * @param card 卡片
     */
    private void setLayoutParams(View card, int index) {
        LayoutParams params = new LayoutParams(card.getLayoutParams());
        params.topMargin = (int) (DensityUtil.getDisplayMetrics(mContext).heightPixels * 0.1) + getResources().getDimensionPixelSize(
                R.dimen.card_item_margin) * index;
        params.bottomMargin = (int) (DensityUtil.getDisplayMetrics(mContext).heightPixels * 0.1) - getResources().getDimensionPixelSize(
                R.dimen.card_item_margin) * index;
        params.leftMargin = (int) (DensityUtil.getDisplayMetrics(mContext).widthPixels * 0.1);
        params.rightMargin = (int) (DensityUtil.getDisplayMetrics(mContext).widthPixels * 0.1);
        card.setLayoutParams(params);
    }

代码一目了然,就是往RelativeLayout中不停的添加View,并设置每个添加进来的View的LayoutParams属性。

卡片的层叠我们已经实现了,剩下的就是拖拽了,View的拖拽其实就是对View触摸事件做出响应的操作,这里需要借助OnTouchListener,并给每个View设置touch事件:

	private int mLastY = 0;
    private int mLastX = 0;
    private int mCardLeft;
    private int mCardTop;
    private int mCardRight;
    private int mCardBottom;
    private boolean mLeftOut = false;
    private boolean mRightOut = false;
    private boolean mOnTouch = true;

    OnTouchListener onTouchListener = new OnTouchListener() {
        @Override
        public boolean onTouch(View v, MotionEvent event) {
            if (mOnTouch) {
                int rawY = (int) event.getRawY();
                int rawX = (int) event.getRawX();
                switch (event.getAction()) {
                    case MotionEvent.ACTION_DOWN:
                        getLayout();
                        mLastY = (int) event.getRawY();
                        mLastX = (int) event.getRawX();
                        break;
                    case MotionEvent.ACTION_MOVE:
                        int offsetY = rawY - mLastY;
                        int offsetX = rawX - mLastX;
                        mCardList.get(0).layout(mCardList.get(0).getLeft() + offsetX, mCardList.get(0).getTop() + offsetY, mCardList.get(0).getRight() + offsetX, mCardList.get(0).getBottom() + offsetY);
                        mRightOut = mCardList.get(0).getLeft() > DensityUtil.getDisplayMetrics(mContext).widthPixels / 2;
                        mLeftOut = mCardList.get(0).getRight() < DensityUtil.getDisplayMetrics(mContext).widthPixels / 2;
                        mLastY = rawY;
                        mLastX = rawX;
                        break;
                    case MotionEvent.ACTION_UP:
                        change();
                        break;
                }
            }
            return true;
        }
    };

View的拖拽是通过layout方法来实现的,那么如何判别View 是向左滑动还是向右滑动,上面有定义了两个属性mRightOut和mLeftOut,mRightOut表示当卡片左边线距离屏幕左边缘的距离超出屏幕一半时认为卡片向右滑出;mLeftOut表示当卡片右边线距离屏幕右边缘的距离小于屏幕一半时认为卡片向左滑出。

滑出的动作交由change()这个方法来实现:

private void change() {
        if (mLeftOut) {
           /*
            往左边滑出
             */
            out(true);
        } else if (mRightOut) {
             /*
            往右边滑出
             */
            out(false);

        } else {
            //复位
            reset();
        }
    }

当mRightOut和mLeftOut都为false时,说明滑动时卡片并没有超出屏幕一半,这时手指离开屏幕时,就应该复位,我们看相关方法:

	 /**
     * 卡片复位
     */
    private void reset() {
        CardIndex oldCardIndex = new CardIndex(mCardLeft, mCardTop, mCardRight, mCardBottom);
        CardIndex newCardIndex = new CardIndex(mCardList.get(0).getLeft(), mCardList.get(0).getTop(), mCardList.get(0).getRight(), mCardList.get(0).getBottom());
        animator(newCardIndex, oldCardIndex);
    }

oldCardIndex是卡片原有位置,newCardIndex是卡片移动后当位置,如果直接通过layout方法来实现复位,会发现卡片复位动作是瞬间就完成的,从视觉效果上来看,感觉比较生硬,因此我们要用到android属性动画中的ValueAnimator.ofObject静态方法,并实现一个TypeEvaluator接口的类:

    class CardIndex {
        int left;
        int top;
        int right;
        int bottom;

        CardIndex(int left, int top, int right, int bottom) {
            this.left = left;
            this.top = top;
            this.right = right;
            this.bottom = bottom;
        }

        int getLeft() {
            return left;
        }

        int getTop() {
            return top;
        }

        int getRight() {
            return right;
        }

        int getBottom() {
            return bottom;
        }
    }

    class PointEvaluator implements TypeEvaluator {

        @Override
        public Object evaluate(float fraction, Object startValue, Object endValue) {
            CardIndex startPoint = (CardIndex) startValue;
            CardIndex endPoint = (CardIndex) endValue;
            int left = (int) (startPoint.getLeft() + fraction * (endPoint.getLeft() - startPoint.getLeft()));
            int top = (int) (startPoint.getTop() + fraction * (endPoint.getTop() - startPoint.getTop()));
            int right = (int) (startPoint.getRight() + fraction * (endPoint.getRight() - startPoint.getRight()));
            int bottom = (int) (startPoint.getBottom() + fraction * (endPoint.getBottom() - startPoint.getBottom()));
            return new CardIndex(left, top, right, bottom);
        }

    }

实现TypeEvaluator接口,然后重写evaluate()方法。evaluate()方法当中传入了三个参数,第一个参数fraction,这个参数用于表示动画的完成度的,我们应该根据它来计算当前动画的值应该是多少,第二第三个参数分别表示动画的初始值和结束值。利用startValue+fraction*(endValue-startValue)公式得到我们的动画值。CardIndex用于保存卡片layout值。


 private void animator(CardIndex newCard, CardIndex oldCard) {

        ValueAnimator animator = ValueAnimator.ofObject(new PointEvaluator(), newCard, oldCard);
        animator.setDuration(200);
        animator.setInterpolator(new AccelerateDecelerateInterpolator());
        animator
                .addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
                    @Override
                    public void onAnimationUpdate(ValueAnimator animation) {
                        CardIndex value = (CardIndex) animation.getAnimatedValue();
                        mCardList.get(0).layout(value.left, value.top, value.right, value.bottom);
                    }
                });
        animator.addListener(new AnimatorListenerAdapter() {
            @Override
            public void onAnimationEnd(Animator animation) {
                if (mRightOut || mLeftOut) {
                    removeTopCard();
                    if (mLeftOrRight != null) {
                        mLeftOrRight.leftOrRight(mLeftOut);
                    }
                }
                mOnTouch = true;
            }
        });
        animator.start();
    }

看完了复位操作,我们再来看看滑出操作:
    /**
     * 卡片滑出
     *
     * @param left 是否向左滑出
     */
    private void out(boolean left) {
        CardIndex oldCardIndex;
        CardIndex newCardIndex;
        if (left) {
            /*
            向左滑出
             */
            oldCardIndex = new CardIndex(-mCardRight, mCardTop, 0, mCardBottom);
            newCardIndex = new CardIndex(mCardList.get(0).getLeft(), mCardList.get(0).getTop(), mCardList.get(0).getRight(), mCardList.get(0).getBottom());
        } else {
            /*
            向右滑出
             */
            oldCardIndex = new CardIndex(DensityUtil.getDisplayMetrics(mContext).widthPixels, mCardTop, DensityUtil.getDisplayMetrics(mContext).widthPixels + (mCardRight - mCardLeft), mCardBottom);
            newCardIndex = new CardIndex(mCardList.get(0).getLeft(), mCardList.get(0).getTop(), mCardList.get(0).getRight(), mCardList.get(0).getBottom());
        }

        animator(newCardIndex, oldCardIndex);
    }

和上面复位的操作类似,只是上面的代码实现了通过动画将卡片滑出屏幕。




具体的实现原理差不多就这些,下面贴出完整源码:

package com.glh.cardview.card;

import android.animation.Animator;
import android.animation.AnimatorListenerAdapter;
import android.animation.TypeEvaluator;
import android.animation.ValueAnimator;
import android.content.Context;
import android.util.AttributeSet;
import android.view.MotionEvent;
import android.view.View;
import android.view.animation.AccelerateDecelerateInterpolator;
import android.widget.RelativeLayout;

import com.glh.cardview.R;
import com.glh.cardview.util.DensityUtil;
import com.glh.cardview.util.ListUtil;

import java.util.ArrayList;

/**
 * 卡片容器
 * Created by glh on 2017-06-08.
 */
public class CardGroupView extends RelativeLayout {

    private Context mContext;

    //指定剩余卡片还剩下多少时加载更多
    private int mLoadSize = 2;
    //是否执行加载更多,加载更多时,卡片依次添加在后面的;而添加卡片时,卡片是依次添加在上面
    private boolean isLoadMore = false;
    //保存当前容器中的卡片
    private ArrayList<View> mCardList = new ArrayList<>();
    //加载更多监听器
    private LoadMore mLoadMore;
    //左右滑动监听器
    private LeftOrRight mLeftOrRight;

    public CardGroupView(Context context) {
        this(context, null);
    }

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

    public CardGroupView(Context context, AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
        this.mContext = context;

    }

    @Override
    public void addView(View card) {
        if (isLoadMore) {
            this.mCardList.add(ListUtil.getSize(mCardList), card);
        } else {
            this.mCardList.add(card);
        }
        LayoutParams layoutParams = new LayoutParams(LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT);
        layoutParams.addRule(RelativeLayout.CENTER_VERTICAL);
        this.addView(card, 0, layoutParams);
        card.setOnTouchListener(onTouchListener);
        if (!isLoadMore) {
            this.setLayoutParams(card, mCardList.size());
        }

    }

    /**
     * 设置卡片LayoutParams
     *
     * @param card 卡片
     */
    private void setLayoutParams(View card, int index) {
        LayoutParams params = new LayoutParams(card.getLayoutParams());
        params.topMargin = (int) (DensityUtil.getDisplayMetrics(mContext).heightPixels * 0.1) + getResources().getDimensionPixelSize(
                R.dimen.card_item_margin) * index;
        params.bottomMargin = (int) (DensityUtil.getDisplayMetrics(mContext).heightPixels * 0.1) - getResources().getDimensionPixelSize(
                R.dimen.card_item_margin) * index;
        params.leftMargin = (int) (DensityUtil.getDisplayMetrics(mContext).widthPixels * 0.1);
        params.rightMargin = (int) (DensityUtil.getDisplayMetrics(mContext).widthPixels * 0.1);
        card.setLayoutParams(params);
    }

    /**
     * 每次移除时需要重置剩余卡片的位置
     */
    private void resetLayoutParams() {
        for (int i = 0; i < mCardList.size(); i++) {
            setLayoutParams(mCardList.get(i), i);
        }
    }

    private int mLastY = 0;
    private int mLastX = 0;
    private int mCardLeft;
    private int mCardTop;
    private int mCardRight;
    private int mCardBottom;
    private boolean mLeftOut = false;
    private boolean mRightOut = false;
    private boolean mOnTouch = true;

    OnTouchListener onTouchListener = new OnTouchListener() {
        @Override
        public boolean onTouch(View v, MotionEvent event) {
            if (mOnTouch) {
                int rawY = (int) event.getRawY();
                int rawX = (int) event.getRawX();
                switch (event.getAction()) {
                    case MotionEvent.ACTION_DOWN:
                        getLayout();
                        mLastY = (int) event.getRawY();
                        mLastX = (int) event.getRawX();
                        break;
                    case MotionEvent.ACTION_MOVE:
                        int offsetY = rawY - mLastY;
                        int offsetX = rawX - mLastX;
                        mCardList.get(0).layout(mCardList.get(0).getLeft() + offsetX, mCardList.get(0).getTop() + offsetY, mCardList.get(0).getRight() + offsetX, mCardList.get(0).getBottom() + offsetY);
                        mRightOut = mCardList.get(0).getLeft() > DensityUtil.getDisplayMetrics(mContext).widthPixels / 2;
                        mLeftOut = mCardList.get(0).getRight() < DensityUtil.getDisplayMetrics(mContext).widthPixels / 2;
                        mLastY = rawY;
                        mLastX = rawX;
                        break;
                    case MotionEvent.ACTION_UP:
                        change();
                        break;
                }
            }
            return true;
        }
    };

    private void getLayout() {
        mCardLeft = mCardList.get(0).getLeft();
        mCardTop = mCardList.get(0).getTop();
        mCardRight = mCardList.get(0).getRight();
        mCardBottom = mCardList.get(0).getBottom();
    }

    private void change() {
        if (mLeftOut) {
           /*
            往左边滑出
             */
            out(true);
        } else if (mRightOut) {
             /*
            往右边滑出
             */
            out(false);

        } else {
            //复位
            reset();
        }
    }

    class CardIndex {
        int left;
        int top;
        int right;
        int bottom;

        CardIndex(int left, int top, int right, int bottom) {
            this.left = left;
            this.top = top;
            this.right = right;
            this.bottom = bottom;
        }

        int getLeft() {
            return left;
        }

        int getTop() {
            return top;
        }

        int getRight() {
            return right;
        }

        int getBottom() {
            return bottom;
        }
    }

    class PointEvaluator implements TypeEvaluator {

        @Override
        public Object evaluate(float fraction, Object startValue, Object endValue) {
            CardIndex startPoint = (CardIndex) startValue;
            CardIndex endPoint = (CardIndex) endValue;
            int left = (int) (startPoint.getLeft() + fraction * (endPoint.getLeft() - startPoint.getLeft()));
            int top = (int) (startPoint.getTop() + fraction * (endPoint.getTop() - startPoint.getTop()));
            int right = (int) (startPoint.getRight() + fraction * (endPoint.getRight() - startPoint.getRight()));
            int bottom = (int) (startPoint.getBottom() + fraction * (endPoint.getBottom() - startPoint.getBottom()));
            return new CardIndex(left, top, right, bottom);
        }

    }

    /**
     * 卡片复位
     */
    private void reset() {
        CardIndex oldCardIndex = new CardIndex(mCardLeft, mCardTop, mCardRight, mCardBottom);
        CardIndex newCardIndex = new CardIndex(mCardList.get(0).getLeft(), mCardList.get(0).getTop(), mCardList.get(0).getRight(), mCardList.get(0).getBottom());
        animator(newCardIndex, oldCardIndex);
    }

    /**
     * 卡片滑出
     *
     * @param left 是否向左滑出
     */
    private void out(boolean left) {
        CardIndex oldCardIndex;
        CardIndex newCardIndex;
        if (left) {
            /*
            向左滑出
             */
            oldCardIndex = new CardIndex(-mCardRight, mCardTop, 0, mCardBottom);
            newCardIndex = new CardIndex(mCardList.get(0).getLeft(), mCardList.get(0).getTop(), mCardList.get(0).getRight(), mCardList.get(0).getBottom());
        } else {
            /*
            向右滑出
             */
            oldCardIndex = new CardIndex(DensityUtil.getDisplayMetrics(mContext).widthPixels, mCardTop, DensityUtil.getDisplayMetrics(mContext).widthPixels + (mCardRight - mCardLeft), mCardBottom);
            newCardIndex = new CardIndex(mCardList.get(0).getLeft(), mCardList.get(0).getTop(), mCardList.get(0).getRight(), mCardList.get(0).getBottom());
        }

        animator(newCardIndex, oldCardIndex);
    }

    private void animator(CardIndex newCard, CardIndex oldCard) {

        ValueAnimator animator = ValueAnimator.ofObject(new PointEvaluator(), newCard, oldCard);
        animator.setDuration(200);
        animator.setInterpolator(new AccelerateDecelerateInterpolator());
        animator
                .addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
                    @Override
                    public void onAnimationUpdate(ValueAnimator animation) {
                        mOnTouch = false;
                        CardIndex value = (CardIndex) animation.getAnimatedValue();
                        mCardList.get(0).layout(value.left, value.top, value.right, value.bottom);
                    }
                });
        animator.addListener(new AnimatorListenerAdapter() {
            @Override
            public void onAnimationEnd(Animator animation) {
                if (mRightOut || mLeftOut) {
                    removeTopCard();
                    if (mLeftOrRight != null) {
                        mLeftOrRight.leftOrRight(mLeftOut);
                    }
                }
                mOnTouch = true;
            }
        });
        animator.start();
    }


    /**
     * 移除顶部卡片(无动画)
     */
    public void removeTopCard() {
        if (!ListUtil.isEmpty(this.mCardList)) {
            removeView(this.mCardList.remove(0));
            if (mCardList.size() == mLoadSize) {
                if (mLoadMore != null) {
                    this.isLoadMore = true;
                    this.mLoadMore.load();
                    this.isLoadMore = false;
                    this.resetLayoutParams();
                }
            }

        }
    }

    /**
     * 移除顶部卡片(有动画)
     *
     * @param left 向左吗
     */
    public void removeTopCard(boolean left) {
        if (this.mOnTouch) {
            this.mLeftOut = left;
            this.mRightOut = !this.mLeftOut;
            this.getLayout();
            this.out(left);
        }
    }

    /**
     * 当剩余卡片等于size时,加载更多
     */
    public void setloadSize(int size) {
        this.mLoadSize = size;
    }

    /**
     * 加载更多监听
     *
     * @param listener {@link LoadMore}
     */
    public void setLoadMoreListener(LoadMore listener) {
        this.mLoadMore = listener;
    }

    /**
     * 左右滑动监听
     *
     * @param listener {@link LeftOrRight}
     */
    public void setLeftOrRightListener(LeftOrRight listener) {
        this.mLeftOrRight = listener;
    }

    public interface LoadMore {
        void load();
    }

    public interface LeftOrRight {
        void leftOrRight(boolean left);
    }

}


下载地址


以下是完整的github项目地址,欢迎多多star和fork。
github项目源码地址:点击【项目源码】

  • 8
    点赞
  • 25
    收藏
    觉得还不错? 一键收藏
  • 5
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 5
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值