什么是层叠式卡片列表,千言万语不如直接看下图:

                        


       不知道为什么在真机上的效果跟制作的git图有差别,大家可以运行代码查看真机上的效果。对,层叠式卡片就是这样,可以向屏幕的任何方向拖动,当拖动的超过一定范围,松手后它会继续延续这个方向滑出屏幕 ,否则会恢复到原点位置。


效果

  • 向任意方向拖动卡片,当拖动的卡片有一半超出父布局,松手后则按拖动的方向滑出屏幕,否则会恢复到原点位置。
  • 可以设置显示的卡片的个数。
  • 可以设置层叠卡片依次缩小的比例。
  • 可以设置层叠卡片层叠的高度。
  • 设置卡片的单击事件。
  • 可以循环显示一个数据集合,当然,你也可以随时更新卡片列表中的内容。


简单使用

先说一下怎么使用,再来介绍它的实现。源码已经上传到github上了,并发布了可依赖库,只要简单的引用,然后就可以使用了。

第一步,在project的根目录的build.gradle添加:

allprojects {repositories {...maven { url 'https://jitpack.io' }}}

             然后在module的目录下的build.gradle文件中添加依赖库:

dependencies {        compile 'com.github.DakTop:android-wake-view:v1.0.3'}

第二步,在布局文件中引用:

       <    com.dak.weakview.layout.WeakCardOverlapLayout             android    :layout_width=    "wrap_content"                  android    :layout_height=    "wrap_content"     />          

第三步,在Activity中:

      

public class WeakCardOverlapActivity extends AppCompatActivity {    private WeakCardOverlapLayout weakcardoverlapLayout;    private WeakCurrencyAdapter adapter;    @Override    protected void onCreate(Bundle savedInstanceState) {        super.onCreate(savedInstanceState);        setContentView(R.layout.activity_weak_card_overlap);        weakcardoverlapLayout = findViewById(R.id.weakcardoverlaplayout);        //设置滑动效果为,滑动时可以覆盖在其他View上面。        weakcardoverlapLayout.setParentClipChild(false);        //设置点击事件        weakcardoverlapLayout.setOnItemClickListener(new OnWeakItemClickListener() {            @Override            public void onWeakItemClickListener(int position, View view) {                Toast.makeText(WeakCardOverlapActivity.this, adapter.getItem(position), Toast.LENGTH_SHORT).show();            }        });        //初始化列表的adapter        adapter = new WeakCurrencyAdapter(this, R.layout.view_weak_overlap) {            @Override            public void notifyItemView(WeakCurrencyViewHold holder, String item, int position) {                holder.setText(R.id.textview, item);            }        };        //设置列表的adapter        weakcardoverlapLayout.setAdapter(adapter);        //初始化数据        adapter.refreshData(MainActivity.list);    }

实现包括:重写RelativeLayout布局,ViewDragHelper控制卡片的拖拽以及滑动、属性动画对卡片的缩放和移动、处理在滑动列表中事件的传递以及封装了简单的Adapter等等还有一些其它细节的东西。

实现思路:首先这个效果就是在一个父容器里面控制几个子View,我们分两个阶段来完成这个效果。第一个阶段,它的初始化状态。初始化状态就是预先在父容器里添加好相应数量的子View,第一个效果就是让这几个子View重叠在一起,所以这里重写的父布局选择RelativeLayout或者FragmeLayout都可以。然后就是让最上面的子View保持原大小不变,它下面的子View依次按比例缩小并向下移动显现出层叠的效果,好了,第一阶段完成。第二阶段,利用ViewDragHelper来控制子View的拖拽,ViewDragHelper是自定义ViewGroup时处理子View拖拽交互的一个帮助类(不知道怎么使用的可以去网上找资源啦,这里不是重点)。利用ViewDragHelper可以拖动父布局内任意的子View,松手后可以控制被拖动的子View自己滑动到原来的位置,或者是滚动到其它位置。假如让被拖动的子View滑出屏幕外,则余下的子View依次放大并且向上移动,转换成第一阶段的初始状态,并且在重叠的子View最下面在添加一个子View以补充重叠子View的个数。这样第二阶段完成,整个重叠卡片列表的思路就是这样。


下面是实现的主要源码,其中数据的填充是通过自己根据RecycleView.Adapter原理写了一个简单的Adapter,来管理数据集合以及卡片集合,完整的实现代码可以点击文尾的GitHub链接查看:

public class WeakCardOverlapLayout extends RelativeLayout implements WeakViewAdapter        .OnNotifyDataLisetener {    private WeakViewAdapter adapter;    private ViewDragHelper viewDragHelper;    private boolean isRemoveChil = false;    private int moveX;    private int moveY;    //层叠卡片的个数    private int cardCount = 3;    //层叠卡片的缩放比例,无缩放为0    private float scaleVal = 0.15f;    //层叠的卡片层次的高度    private int viewStackUpHeight = 15;    //子View状态初始化标识    private boolean initChilState = false;    //最底层的子View对应的Adapter数据集合中的位置    private int position = 0;    private OnWeakItemClickListener itemClickListener;    private int screenHeight;    private int screenWidth;    private int bottomViewHolderPosition = cardCount - 1;    public WeakCardOverlapLayout(@NonNull Context context, @Nullable AttributeSet attrs) {        super(context, attrs);        this.setClipChildren(false);        viewStackUpHeight = Tool.dip2px(context, viewStackUpHeight / 2);        viewDragHelper = ViewDragHelper.create(this, 1.0f, new DragCallbackImpl());        screenHeight = Tool.getScreenHeight(context);        screenWidth = Tool.getScreenWidth(context);    }    /**     * 设置子View是否能超过父View边界。     *     * @param c     */    public void setParentClipChild(boolean c) {        ViewGroup viewGroup = (ViewGroup) getParent();        if (viewGroup != null) {            viewGroup.setClipChildren(false);        }    }    public void setAdapter(WeakViewAdapter adapter) {        this.adapter = adapter;        adapter.setViewGroupParent(this);        adapter.setOnNotifyDataLisetener(this);    }    @Override    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {        super.onMeasure(widthMeasureSpec, heightMeasureSpec);        if (getChildCount() > 1) {            //当显示的View大于1的时候,就会出现层叠效果,此时父ViewGroup需要重新计算高度,即需要增加高度,而增加的在这个高度就是设置viewStackUpHeight乘以卡片个数减一。            setMeasuredDimension(getMeasuredWidth(), getMeasuredHeight() + (viewStackUpHeight * (getChildCount() - 1)));        }    }    @Override    protected void onSizeChanged(int w, int h, int oldw, int oldh) {        super.onSizeChanged(w, h, oldw, oldh);        if (initChilState) {            int size = getChildCount();            for (int i = 0; i < size; i++) {                initChilState(size - 1 - i, getChildAt(i));            }            initChilState = false;        }    }    @Override    public boolean onInterceptTouchEvent(MotionEvent ev) {        final int action = ev.getAction() & MotionEventCompat.ACTION_MASK;        switch (action) {            case MotionEvent.ACTION_MOVE:                requestDisallowInterceptTouchEventAll();                break;            case MotionEvent.ACTION_DOWN:                requestDisallowInterceptTouchEventAll();                break;        }        return viewDragHelper.shouldInterceptTouchEvent(ev);    }    @Override    public boolean onTouchEvent(MotionEvent event) {        switch (event.getAction()) {            case MotionEvent.ACTION_MOVE:                requestDisallowInterceptTouchEventAll();                break;            case MotionEvent.ACTION_CANCEL:                requestDisallowInterceptTouchEventAll();                break;        }        viewDragHelper.processTouchEvent(event);        return true;    }    class DragCallbackImpl extends ViewDragHelper.Callback {        @Override        public boolean tryCaptureView(View child, int pointerId) {            //这里判断只能滑动第一个View,并且只有一个View的时候不能滑动。            return getChildCount() <= 1 ? false : getChildAt(getChildCount() - 1) == child;        }        @Override        public int clampViewPositionHorizontal(View child, int left, int dx) {            //这里控制滑动View的横向边界            return left;        }        @Override        public int clampViewPositionVertical(View child, int top, int dy) {            //这里控制滑动View的纵向边界            return top;        }        @Override        public int getViewHorizontalDragRange(View child) {            return Integer.MAX_VALUE;        }        @Override        public int getViewVerticalDragRange(View child) {            return Integer.MAX_VALUE;        }        /**         * 拖动View松手时调用此方法。         *         * @param releasedChild         * @param xvel         * @param yvel         */        @Override        public void onViewReleased(View releasedChild, float xvel, float yvel) {            float curX = releasedChild.getX();            float curY = releasedChild.getY();            int vW = releasedChild.getWidth();            int vH = releasedChild.getHeight();            //如果在X或者Y抽方向上移动的距离大于卡片宽度或者高度一半的话,则移除屏幕之外            if (Math.abs(curX) > vW / 2 || Math.abs(curY) > vH / 2) {                moveX = (int) (curX);                moveY = (int) (curY);                while (Math.abs(moveX) < screenWidth || Math.abs(moveY) < screenHeight) {                    moveX += curX;                    moveY += curY;                }            } else {                moveX = 0;                moveY = 0;            }            isRemoveChil = viewDragHelper.settleCapturedViewAt(moveX, moveY);            invalidate();        }        /**         * 正在被拖动的View或者自动滚动的View的位置改变时会调用此方法。         *         * @param changedView         * @param left         * @param top         * @param dx         * @param dy         */        @Override        public void onViewPositionChanged(View changedView, int left, int top, int dx, int dy) {            if (!isRemoveChil)                return;            //这里首先判断正在改变位置的View是否是刚刚拖动松手后滑出屏幕外的View,如果是则继续判断,它是否已经完全滑动出屏幕外。            int[] location = new int[2];            changedView.getLocationInWindow(location); //获取在当前窗口内的绝对坐标            changedView.getLocationOnScreen(location);//获取在整个屏幕内的绝对坐标            if (moveX != 0 && moveY != 0 && Math.abs(left) >= Math.abs(moveX) && Math.abs(top) >= Math.abs(moveY)) {                moveX = 0;                moveY = 0;                notifyView();                isRemoveChil = false;            }        }    }    private void requestDisallowInterceptTouchEventAll() {        getParent().requestDisallowInterceptTouchEvent(true);    }    @Override    public void computeScroll() {        if (viewDragHelper.continueSettling(true)) {            ViewCompat.postInvalidateOnAnimation(this);        }    }    @Override    public void onInsertNotifyDataLisetener() {        //这里控制显示的view的个数不能超过设置的cardSize。        int insertCount = Math.min(cardCount, adapter.getItemCount());        int childCount = getChildCount();        if (childCount > 0) {            if (childCount >= cardCount) {                return;            } else {                insertCount = insertCount - childCount;            }        }        for (int i = 0; i < insertCount; i++) {            addBottomView(i, adapter.getHolderView(i + childCount));        }        bottomViewHolderPosition = insertCount - 1;        position = 0;        initChilState = true;    }    /**     * 当把第一个卡片滑出屏幕后需要更新界面以及ViewHolder列表。     */    private void notifyView() {        //第一个ViewHolder滑出屏幕后,将这个ViewHolder添加到ViewHolder列表的尾部,并移除列表的第一个ViewHolder以起到View复用以及循环在展示列表数据集的效果        adapter.getViewHolderList().remove(0);        adapter.getViewHolderList().add(adapter.onCreateViewHolder(this));        int dataSize = adapter.getItemCount();        adapter.notifyItemViewWithHolder(dataSize - 1, position);        position++;        if (position >= dataSize) {            position = 0;        }        int chilCount = getChildCount();        //记录下被划出屏幕的View在Y轴上移动的距离。        float lastTransY = getChildAt(chilCount - 1).getTranslationY();        //删除被滑出屏幕的View        removeViewAt(chilCount - 1);        chilCount = getChildCount();        //缩放和移动剩下的View。        for (int i = chilCount - 1; i >= 0; i--) {            View view = getChildAt(i);            //获取当前View的在Y轴上的移动距离            float thisTransY = view.getTranslationY();            //当前View只要移动上一个View在Y轴上移动的距离即可。            ValueAnimator translation = ObjectAnimator.ofFloat(view, "translationY", lastTransY);            //当前View放大到滑出屏幕的View的大小,他们的差值为scaleVal。            ValueAnimator scaleX = ObjectAnimator.ofFloat(view, "scaleX", view.getScaleX() + scaleVal);            ValueAnimator scaleY = ObjectAnimator.ofFloat(view, "scaleY", view.getScaleY() + scaleVal);            translation.setDuration(150);            scaleX.setDuration(150);            scaleY.setDuration(150);            AnimatorSet animatorSet = new AnimatorSet();            animatorSet.play(translation).with(scaleX).with(scaleY);            animatorSet.start();            //用lastTransY变量记录当前View的在Y轴上的移动距离            lastTransY = thisTransY;        }        int cardBottomPosition = Math.min(cardCount, dataSize) - 1;        View view = adapter.getHolderView(cardBottomPosition);        bottomViewHolderPosition++;        if (bottomViewHolderPosition >= dataSize) {            bottomViewHolderPosition = 0;        }        addBottomView(bottomViewHolderPosition, view);        initChilState(cardBottomPosition, view);    }    private void addBottomView(final int position, final View view) {        view.setOnClickListener(new OnClickListener() {            @Override            public void onClick(View v) {                if (itemClickListener != null) {                    itemClickListener.onWeakItemClickListener(position, view);                }            }        });        addView(view, 0);    }    /**     * 在底部补添一个View     *     * @param position     */    private void initChilState(final int position, View view) {        //等于第一个无缩放View的高度。        int firstViewH = getChildAt(getChildCount() - 1).getMeasuredHeight();        //计算当前View的缩放值,按照设置的scaleVal值依次递减。        float scale = 1 - (position * scaleVal);        view.setScaleX(scale);        view.setScaleY(scale);        //被添加的所有的卡片缩放后都是在父布局的正中心的,所以要计算每个卡片需要向下移动的高度来显示出层叠的效果。这个高度可以根据下面的公式自己理解。        float ty = ((firstViewH - (firstViewH * scale)) / 2) + viewStackUpHeight * position;        view.setTranslationY(ty);    }    @Override    public void onDeleteNotifyDataLisetener() {        int removeCount = getChildCount() - adapter.getViewHolderCount();        while (removeCount > 0) {            this.removeViewAt(0);            removeCount--;        }    }    public WeakViewAdapter getAdapter() {        return adapter;    }    /**     * 显示卡片的个数     *     * @param cardCount     */    public void setCardCount(int cardCount) {        this.cardCount = cardCount;    }    /**     * 卡片底部层叠层次的高度。     *     * @param viewStackUpHeight     */    public void setViewStackUpHeight(int viewStackUpHeight) {        this.viewStackUpHeight = viewStackUpHeight;    }    public void setOnItemClickListener(OnWeakItemClickListener itemClickListener) {        this.itemClickListener = itemClickListener;    }    /**     * 设置卡片一次缩小比例     *     * @param scaleVal 0-1;     */    public void setScaleVal(float scaleVal) {        this.scaleVal = scaleVal;    }}
https://github.com/DakTop/android-wake-view



更多相关文章

  1. RecyclerView嵌套ScrollView,滑动卡顿解决方案,滑动冲突解决方案
  2. Android标题栏随滑动渐变效果的实现
  3. Android中实现仿微信界面切换平滑滑动效果
  4. ViewFlipper动态加载View
  5. Android(安卓)ViewPager不可滑动
  6. android拖动imageview实现复制效果
  7. android ListView向上滑动隐藏标题,下拉显示标题栏
  8. Android(安卓)---- 侧滑删除菜单的实现
  9. Android(安卓)UI(三)SlidingMenu实现滑动菜单(详细 官方)

随机推荐

  1. 我的Android(安卓)NDK之旅(二),使用ndk-buil
  2. Android弹出DatePickerDialog并获取值的
  3. 获取Android手机的图片和视频的缩略图[转
  4. (1)LruCache原理分析
  5. Android获取常用辅助方法(获取屏幕高度、
  6. android2.2应用开发之IccCard(sim卡或USIM
  7. Android(安卓)进程管理概述
  8. Android(安卓)Design Library之四:BottomS
  9. Android播放音乐案例分享
  10. Notification探险