Android(安卓)可拖拽层叠式卡片列表——WeakView系列
什么是层叠式卡片列表,千言万语不如直接看下图:
不知道为什么在真机上的效果跟制作的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 更多相关文章
- RecyclerView嵌套ScrollView,滑动卡顿解决方案,滑动冲突解决方案
- Android标题栏随滑动渐变效果的实现
- Android中实现仿微信界面切换平滑滑动效果
- ViewFlipper动态加载View
- Android(安卓)ViewPager不可滑动
- android拖动imageview实现复制效果
- android ListView向上滑动隐藏标题,下拉显示标题栏
- Android(安卓)---- 侧滑删除菜单的实现
- Android(安卓)UI(三)SlidingMenu实现滑动菜单(详细 官方)