转载请标明出处:
https://blog.csdn.net/AnalyzeSystem/article/details/80078954
本文出自AnalyzeSystem的博客
本篇博客只为卡片叠层相关开源库系列学习理解分析,为可能存在的类似需求定制自定义控件做一个技术累计分享。
系统自带了横向Viewpager,没有纵向所以自定义了VerticalViewPager,然后扩展横向纵向的ViewPager:HorizontalInfiniteCycleViewPager、VerticalInfiniteCycleViewPager,自定义的InfiniteCycleManager 主要负责自定义属性的解析以及相关属性的代码设置,内部持有自定义InfiniteCycleScroller,InfiniteCycleScroller主要是修改startScroll的duration参数,VerticalInfiniteCycleViewPager构造参数传入默认1000
两个扩展ViewPager主要是一些自定义属性、listener、adapter相关的代理,都实现了ViewPageable,相关函数默认值set get都在InfiniteCycleManager,这里就不细说了,本篇核心卡片效果
扩展的ViewPager会初始化InfiniteCycleManager构造函数把自定义的scroller通过反射设置给viewpager,接着通过processAttributeSet函数解析自定义属性
public InfiniteCycleManager(final Context context,final ViewPageable viewPageable,final AttributeSet attributeSet) {
mContext = context;
mIsVertical = viewPageable instanceof VerticalViewPager;
mViewPageable = viewPageable;
mCastViewPageable = (View) viewPageable;
// Set default InfiniteViewPager
mViewPageable.setPageTransformer(false, getInfinityCyclePageTransformer());
mViewPageable.addOnPageChangeListener(mInfinityCyclePageChangeListener);
mViewPageable.setClipChildren(DEFAULT_DISABLE_FLAG);
mViewPageable.setDrawingCacheEnabled(DEFAULT_DISABLE_FLAG);
mViewPageable.setWillNotCacheDrawing(DEFAULT_ENABLE_FLAG);
mViewPageable.setPageMargin(DEFAULT_PAGE_MARGIN);
mViewPageable.setOffscreenPageLimit(DEFAULT_OFFSCREEN_PAGE_LIMIT);
mViewPageable.setOverScrollMode(OVER_SCROLL_NEVER);
// Reset scroller and process attribute set
resetScroller();
processAttributeSet(attributeSet);
}
viewPager的滑动触摸事件拦截分发也通过代理由InfiniteCycleManager来处理
@Override
public boolean onTouchEvent(final MotionEvent ev) {
try {
return mInfiniteCycleManager == null ? super.onTouchEvent(ev) :
mInfiniteCycleManager.onTouchEvent(ev) && super.onTouchEvent(ev);
} catch (IllegalArgumentException e) {
return true;
}
}
@Override
public boolean onInterceptTouchEvent(final MotionEvent ev) {
try {
return mInfiniteCycleManager == null ? super.onInterceptTouchEvent(ev) :
mInfiniteCycleManager.onInterceptTouchEvent(ev) && super.onInterceptTouchEvent(ev);
} catch (IllegalArgumentException e) {
return true;
}
}
Touch函数有个知识点:检查触摸位置是否超出界限
private void checkHitRect(final MotionEvent event) {
if (event.getAction() == MotionEvent.ACTION_DOWN) {
mHitRect.set(
mCastViewPageable.getLeft(), mCastViewPageable.getTop(),
mCastViewPageable.getRight(), mCastViewPageable.getBottom()
);
} else if (event.getAction() == MotionEvent.ACTION_MOVE && !mHitRect.contains(
mCastViewPageable.getLeft() + (int) event.getX(),
mCastViewPageable.getTop() + (int) event.getY()
)) event.setAction(MotionEvent.ACTION_UP);
}
以上内容并不是核心点,viewPager的滑动偏移量变化关联Item动画效果,如果你认真看了上面的代码块应该能记得这两句代码
mViewPageable.setPageTransformer(false, getInfinityCyclePageTransformer());
mViewPageable.addOnPageChangeListener(mInfinityCyclePageChangeListener);
该库的庐山真面目出来了:PageTransformer + OnPageChangeListener,这里补充一个知识点:
作者库里用的ViewPager.SimpleOnPageChangeListener抽象类,SimpleOnPageChangeListener抽象类实现OnPageChangeListener,重写了每个函数,我们OnPageChangeListener = new SimpleOnPageChangeListener好处可以不用重载每个函数
/**
* Apply a property transformation to the given page.
*
* @param page Apply the transformation to this page
* @param position Position of page relative to the current front-and-center
* position of the pager. 0 is front and center. 1 is one full
* page position to the right, and -1 is one page position to the left.
*/
public void transformPage(View view, float position)
transformPage()方法的关键在于position的理解,从doc注释来看,当前选中的item的position永远是0(这与ViewPager的OnPageChangeListener回调方法中的position不同),被选中item的前一个为-1,被选中item的后一个为1。* 其实这里文档的描述并不是完全正确的,前后item position为-1和1的前提是你没有给ViewPager设置pageMargin(通过调用viewPager.setPageMargin(int)方法设置)*。如果你设置了pageMargin,前后item的position需要分别加上(或减去,前减后加)一个偏移量(偏移量的计算方式为pageMargin / pageWidth)。
在用户滑动界面的时候,position是动态变化的,下面以左滑为例:
选中item position:0->-1 - offset (pageMargin / pageWidth)
前一个item position:-1 - offset (pageMargin / pageWidth) -> -2 - offset (pageMargin / pageWidth),再往前就以此类推
后一个item position:1 + offset (pageMargin / pageWidth) -> 0,再往后就以此类推
因此我们可以将position的值应用于setAlpha(), setTranslationX(), 或者 setScaleY()等等方法,从而实现自定义的动画效果。
transformPage函数还有个view.bringToFront()方法的调用,该方法理解可参考下面链接
https://www.cnblogs.com/zhainanJohnny/articles/3292563.html
InfiniteCycleManager 构造函数初始化时为ViewPager还设置了下面两个属性
mViewPageable.setClipChildren(DEFAULT_DISABLE_FLAG);
mViewPageable.setOffscreenPageLimit(DEFAULT_OFFSCREEN_PAGE_LIMIT);
这样ViewPager就可以同时显示多个Item,配合PageTransformer动态计算左右偏移量执行平移缩放动画
ViewPager的无限循环原理通过修改adapter实现,如果有需求如下:
触摸-1 或者1 position位置需要把滑动事件交给 0 position处理
如果有这个问题可以尝试把ViewPager的parent touch事件交给ViewPager
以前群里有人问app 土巴兔的选择装修风格的效果,实现原理也是如此,InfiniteCycleViewPager的学习分析比较粗,凑合着看吧
该库的实现核心知识点在于ViewDragHelper和自定义ViewGroup,关于ViewDragHelper博主以前也撸过一篇博客,有兴趣可以看看
https://blog.csdn.net/AnalyzeSystem/article/details/50537927
首先来重新认识几个类:
OnGestureListener这个是常用的接口,作用手势识别的回调
OnDoubleTapListener双击监听接口
SimpleOnGestureListener 以实现上面两个接口
OnGestureListener和OnDoubleTapListener接口里的函数都是强制必须重写的,即使用不到也要重写出来一个空函数但在 SimpleOnGestureListener类的实例或派生类中不必如此,可以根据情况,用到哪个函数就重写哪个函数,因为 SimpleOnGestureListener类本身已经实现了这两个接口的所有函数,只是里面全是空的而已。
拖动状态下拦截touch事件,touch事件的拦截与处理都交给mDraghelper来处理
/* touch事件的拦截与处理都交给mDraghelper来处理 */
@Override
public boolean onInterceptTouchEvent(MotionEvent ev) {
boolean shouldIntercept = mDragHelper.shouldInterceptTouchEvent(ev);
boolean moveFlag = moveDetector.onTouchEvent(ev);
int action = ev.getActionMasked();
if (action == MotionEvent.ACTION_DOWN) {
// ACTION_DOWN的时候就对view重新排序
if (mDragHelper.getViewDragState() == ViewDragHelper.STATE_SETTLING) {
mDragHelper.abort();
}
orderViewStack();
// 保存初次按下时arrowFlagView的Y坐标
// action_down时就让mDragHelper开始工作,否则有时候导致异常
mDragHelper.processTouchEvent(ev);
}
return shouldIntercept && moveFlag;
}
class MoveDetector extends SimpleOnGestureListener {
@Override
public boolean onScroll(MotionEvent e1, MotionEvent e2, float dx,
// 拖动了,touch不往下传递
return Math.abs(dy) + Math.abs(dx) > mTouchSlop;
}
}
@Override
public boolean onTouchEvent(MotionEvent e) {
try {
// 统一交给mDragHelper处理,由DragHelperCallback实现拖动效果
// 该行代码可能会抛异常,正式发布时请将这行代码加上try catch
mDragHelper.processTouchEvent(e);
} catch (Exception ex) {
ex.printStackTrace();
}
return true;
}
以上代码为事件拦截分发,但整体流程是怎么样的呢?
① setAdapter 调用 doBindAdapter
根据数据不断地addView 添加CardItemView子view,布局动态变化回调函数onLayout重新测量,根据偏移量、步长使子view动态缩放、调整重心调整。
② registerDataSetObserver
Observable是观察者模式的典型应用。在Android下,Observable是一个泛型的抽象类,表示一个观察者对象,提供了观察者注册、反注册及清空三个方法,DataSetObservable在很多的Adapter中都用到,像BaseAdapter。DataSetObservable使用DataSetObserver实例化了Observable。DataSetObserver表示了一个数据集对象的观察者,主要提供了两个方法:
public abstract class DataSetObserver {
@Override
public void onChanged() {
xx.offsetLeftAndRight()...
}
@Override
public void onInvalidated() {
}
}
onChanged 对View重新排序,执行相关透明动画,如果之前就没有数据,需要保存第一条数据,如果第一条数据不等的话,需要重置弱引用缓存
③ ViewDragHelper . DragHelperCallback
用户手势拖动通过上面的事件拦截交给ViewDragHelper处理,ViewDragHelper拖拽效果的主要逻辑DragHelperCallback
判断当前View是否能拖拽,如果可以请求拦截Touch事件,拖动时上面提到的DataSetObserver.onChange函数(onLayout同理)会调用到offsetLeftAndRight导致viewPosition改变,会调到此处onViewPosChanged,所以onViewPosChanged对index做保护处理,并且此时顶层卡片View位置改变,底层的位置也需要调整
public void onViewPosChanged(CardItemView changedView) {
// 调用offsetLeftAndRight导致viewPosition改变,会调到此处,所以此处对index做保护处理
int index = viewList.indexOf(changedView);
if (index + 2 > viewList.size()) {
return;
}
processLinkageView(changedView);
}
/**
* 顶层卡片View位置改变,底层的位置需要调整
*
* @param changedView 顶层的卡片view
*/
private void processLinkageView(View changedView) {
int changeViewLeft = changedView.getLeft();
int changeViewTop = changedView.getTop();
int distance = Math.abs(changeViewTop - initCenterViewY)
+ Math.abs(changeViewLeft - initCenterViewX);
float rate = distance / (float) MAX_SLIDE_DISTANCE_LINKAGE;
float rate1 = rate;
float rate2 = rate - 0.1f;
if (rate > 1) {
rate1 = 1;
}
if (rate2 < 0) {
rate2 = 0;
} else if (rate2 > 1) {
rate2 = 1;
}
ajustLinkageViewItem(changedView, rate1, 1);
ajustLinkageViewItem(changedView, rate2, 2);
CardItemView bottomCardView = viewList.get(viewList.size() - 1);
bottomCardView.setAlpha(rate2);
}
更为详细的说明请阅读源码,有中文注释说明,这个库的作者良心之作啊,满满的中文注释,必须star!!
特别提示:在学习该库的时候一定要建立好模型,有一定ViewDragHelper基础,了解一下“步长”、偏移量等相关知识
CardSwipeLayout库代码层次比较分明,理解起来也比较容易,实现核心关联类:
RecyclerView.LayoutManager、ItemTouchHelper.Callback
博主对ItemTouchHelper不熟悉,于是乎撸了一篇ItemTouchHelper解析相关博客,访问地址
https://blog.csdn.net/AnalyzeSystem/article/details/80165740
在你看过这篇文章或者对ItemTouchHelper有了一定的基础认知后,再来看CardSwipeLayout库,感觉 so easy !!!
CardConfig就一些基础变量,OnSwipeListener接口回调定义,核心点在Callback、LayoutManager。
CardLayoutManager .onLayoutChildren()函数根据position动态缩放Item并并调整Item的位置
@Override
public void onLayoutChildren(final RecyclerView.Recycler recycler, RecyclerView.State state) {
detachAndScrapAttachedViews(recycler);
int itemCount = getItemCount();
// 当数据源个数大于最大显示数时
if (itemCount > CardConfig.DEFAULT_SHOW_ITEM) {
for (int position = CardConfig.DEFAULT_SHOW_ITEM; position >= 0; position--) {
final View view = recycler.getViewForPosition(position);
addView(view);
measureChildWithMargins(view, 0, 0);
int widthSpace = getWidth() - getDecoratedMeasuredWidth(view);
int heightSpace = getHeight() - getDecoratedMeasuredHeight(view);
// recyclerview 布局
layoutDecoratedWithMargins(view, widthSpace / 2, heightSpace / 2,
widthSpace / 2 + getDecoratedMeasuredWidth(view),
heightSpace / 2 + getDecoratedMeasuredHeight(view));
if (position == CardConfig.DEFAULT_SHOW_ITEM) {
view.setScaleX(1 - (position - 1) * CardConfig.DEFAULT_SCALE);
view.setScaleY(1 - (position - 1) * CardConfig.DEFAULT_SCALE);
view.setTranslationY((position - 1) * view.getMeasuredHeight() / CardConfig.DEFAULT_TRANSLATE_Y);
} else if (position > 0) {
view.setScaleX(1 - position * CardConfig.DEFAULT_SCALE);
view.setScaleY(1 - position * CardConfig.DEFAULT_SCALE);
view.setTranslationY(position * view.getMeasuredHeight() / CardConfig.DEFAULT_TRANSLATE_Y);
} else {
view.setOnTouchListener(mOnTouchListener);
}
}
} else {
// 当数据源个数小于或等于最大显示数时
for (int position = itemCount - 1; position >= 0; position--) {
final View view = recycler.getViewForPosition(position);
addView(view);
measureChildWithMargins(view, 0, 0);
int widthSpace = getWidth() - getDecoratedMeasuredWidth(view);
int heightSpace = getHeight() - getDecoratedMeasuredHeight(view);
// recyclerview 布局
layoutDecoratedWithMargins(view, widthSpace / 2, heightSpace / 2,
widthSpace / 2 + getDecoratedMeasuredWidth(view),
heightSpace / 2 + getDecoratedMeasuredHeight(view));
if (position > 0) {
view.setScaleX(1 - position * CardConfig.DEFAULT_SCALE);
view.setScaleY(1 - position * CardConfig.DEFAULT_SCALE);
view.setTranslationY(position * view.getMeasuredHeight() / CardConfig.DEFAULT_TRANSLATE_Y);
} else {
view.setOnTouchListener(mOnTouchListener);
}
}
}
}
onChildDraw:在item绘制时调用,主要操作一些属性动画具体实现自行参考源码
滑动移除Item,并通过自定义的Listener回调到主线程
@Override
public void onSwiped(RecyclerView.ViewHolder viewHolder, int direction) {
// 移除 onTouchListener,否则触摸滑动会乱了
viewHolder.itemView.setOnTouchListener(null);
int layoutPosition = viewHolder.getLayoutPosition();
T remove = dataList.remove(layoutPosition);
adapter.notifyDataSetChanged();
if (mListener != null) {
mListener.onSwiped(viewHolder, remove, direction == ItemTouchHelper.LEFT ? CardConfig.SWIPED_LEFT : CardConfig.SWIPED_RIGHT);
}
// 当没有数据时回调 mListener
if (adapter.getItemCount() == 0) {
if (mListener != null) {
mListener.onSwipedClear();
}
}
}
看似简单么?错觉而已!!其实还是需要一定的技术功底才能写出来的,只是本篇博客分析的比较粗浅
还有一种卡片实现方案AdapterView的修改,由于目前主流使用RecyclerView系列了,这里就不过多了解了,如果你还想学习可以在下面的学习资料里找到。好吧,博主就是懒不想写了!!博主还有工作需要交接…
学习资料来自 github 开源库
https://github.com/Devlight/InfiniteCycleViewPager
https://github.com/xmuSistone/CardSlidePanel
https://github.com/yuqirong/CardSwipeLayout
https://github.com/mcxtzhang/ZLayoutManager
https://github.com/NateRobinson/CardStackViewpager
https://github.com/xiepeijie/SwipeCardView
https://github.com/flschweiger/SwipeStack
https://github.com/fashare2015/StackLayout
https://github.com/OCNYang/PageTransformerHelp
https://github.com/aohanyao/ViewPagerCardTransformer
https://github.com/zhuchen1109/Swipe-cards
https://github.com/BakerJQ/Android-InfiniteCards
https://github.com/czy1121/turncardlistview
博客
https://www.jianshu.com/p/722ece163629
https://blog.csdn.net/u012702547/article/details/52334161