Android开发之卡片叠层实现原理解析

版权声明:本文为博主原创文章,未经博主允许不得转载。 https://blog.csdn.net/AnalyzeSystem/article/details/80078954

转载请标明出处:
https://blog.csdn.net/AnalyzeSystem/article/details/80078954
本文出自AnalyzeSystem的博客

本篇博客只为卡片叠层相关开源库系列学习理解分析,为可能存在的类似需求定制自定义控件做一个技术累计分享。


InfiniteCycleViewPager

系统自带了横向Viewpager,没有纵向所以自定义了VerticalViewPager,然后扩展横向纵向的ViewPager:HorizontalInfiniteCycleViewPagerVerticalInfiniteCycleViewPager,自定义的InfiniteCycleManager 主要负责自定义属性的解析以及相关属性的代码设置,内部持有自定义InfiniteCycleScroller,InfiniteCycleScroller主要是修改startScroll的duration参数,VerticalInfiniteCycleViewPager构造参数传入默认1000

两个扩展ViewPager主要是一些自定义属性、listener、adapter相关的代理,都实现了ViewPageable,相关函数默认值set get都在InfiniteCycleManager,这里就不细说了,本篇核心卡片效果

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好处可以不用重载每个函数

PageTransformer
/** 
 * 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的学习分析比较粗,凑合着看吧


CardSlidePanel

该库的实现核心知识点在于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


CardSwipeLayout库代码层次比较分明,理解起来也比较容易,实现核心关联类:

RecyclerView.LayoutManagerItemTouchHelper.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

猜你喜欢

转载自blog.csdn.net/AnalyzeSystem/article/details/80078954
今日推荐