前几天看到群里的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));
作者:verzqli