Android从零开搞系列:自定义View(14)仿天天美剧拖动卡片的效果(上)

标签: android  github  开源

转载请注意:http://blog.csdn.net/wjzj000/article/details/73432852

本菜开源的一个自己写的Demo,希望能给Androider们有所帮助,水平有限,见谅见谅…


我和一帮应届生同学维护了一个公众号:IT面试填坑小分队。旨在帮助应届生从学生过度到开发者,并且每周树立学习目标,一同进步!
这里写图片描述

写在前面

一晃大三即将结束,这学期真是出奇的快。快到还没感受春天的清香,就进入了热出翔的夏天…mmp,真的热,热到翻个身一身汗….不过,好在学习使我快乐,学习使我快乐,学习使我快乐!

今天记录一篇开源项目的分析。原作者项目GitHub:https://github.com/Diolor/Swipecards

开始

开始之前让我们先瞅一下效果:
这里写图片描述


看过效果,我们来看一下项目整体,了解一下大体的结构:

这里写图片描述

我们来通过源码结构来分析一波:
红线所划掉的类是Activity,也就是这个控件的使用。因此它不属于咱们分析结构的部分。
SwipeFlingAdapterView这个类必然是我们所要使用的这个自定义View,既然结尾是AdapterView那么,这个自定义View很可能设计模式参考了同样继承了AdapterView的ListView等同类控件。
FlingCardListener必然是负责回调,这个毋庸置疑…
只是这个LinearRegression比较不好理解,那么就让咱们在分析代码的时候去好好理解一番!


使用方式


swipeCard.setAdapter(arrayAdapter);
swipeCard.setFlingListener(new SwipeFlingAdapterView.onFlingListener() {
    //重写相应的回调方法
}

通过它的用法,我们可以看到和ListView的用法基本类似,和我们开头分析的一样。
那么就让我们看一看这个自定义View的setAdapter是如何处理的。

SwipeFlingAdapterView


setAdapter方法( ):

如果我们看了ListView的setAdapter方法,我们会发现二者思路基本相同,无非了考虑的情况精细与否。

    @Override
    public void setAdapter(Adapter adapter) {
        if (mAdapter != null && mDataSetObserver != null) {
            mAdapter.unregisterDataSetObserver(mDataSetObserver);
            mDataSetObserver = null;
        }

        mAdapter = adapter;

        if (mAdapter != null  && mDataSetObserver == null) {
            //简单的继承DataSetObserver
            mDataSetObserver = new AdapterDataSetObserver();
            mAdapter.registerDataSetObserver(mDataSetObserver);
        }
    }

AdapterDataSetObserver:

    private class AdapterDataSetObserver extends DataSetObserver {
        @Override
        public void onChanged() {
            requestLayout();
        }

        @Override
        public void onInvalidated() {
            requestLayout();
        }

    }

接下来让我们看一看比较核心的方法,重点的地方已经直接写在了注释之中。

onLayout( )方法:


    @Override
    protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
        super.onLayout(changed, left, top, right, bottom);
        if (mAdapter == null) {
            return;
        }

        mInLayout = true;
        final int adapterCount = mAdapter.getCount();
        //如果adapter中没有对象,清空View
        if(adapterCount == 0) {
            removeAllViewsInLayout();
        }else {
            //获取最上层的Card,LAST_OBJECT_IN_STACK初始化为0
            View topCard = getChildAt(LAST_OBJECT_IN_STACK);
            if(mActiveCard!=null && topCard!=null && topCard==mActiveCard) {
                if (this.flingCardListener.isTouching()) {
                    //此处通过Listener的getLastPoint()拿到Card的坐标信息
                    PointF lastPoint = this.flingCardListener.getLastPoint();
                    //第一次时mLastTouchPoint必然为null,此时进行拖动赋值
                    if (this.mLastTouchPoint == null || !this.mLastTouchPoint.equals(lastPoint)) {
                        this.mLastTouchPoint = lastPoint;
                        //移除第一个Card,加载下面的Card
                        removeViewsInLayout(0, LAST_OBJECT_IN_STACK);
                        //加载下面的Card ( 此方法具体内容在下面展开 )
                        layoutChildren(1, adapterCount);
                    }
                }
            }else{
                //重置UI并设置顶视图监听器
                removeAllViewsInLayout();
                layoutChildren(0, adapterCount);
                // ( 此方法具体内容在下面展开 )
                setTopView();
            }
        }

layoutChildren( )方法:


    private void layoutChildren(int startingIndex, int adapterCount){
        while (startingIndex < Math.min(adapterCount, MAX_VISIBLE) ) {
            //获取移除后的下一个Card
            View newUnderChild = mAdapter.getView(startingIndex, null, this);
            if (newUnderChild.getVisibility() != GONE) {
                //显示移除后的下一个Card ( 此方法具体内容在下面展开 )
                makeAndAddView(newUnderChild);
                //LAST_OBJECT_IN_STACK赋值成当前的Card的索引
                LAST_OBJECT_IN_STACK = startingIndex;
            }
            //自增
            startingIndex++;
        }
    }

makeAndAddView( )方法:


    private void makeAndAddView(View child) {
        FrameLayout.LayoutParams lp = (FrameLayout.LayoutParams) child.getLayoutParams();
        /**
          * 将child加入到ViewGroup中
          * true:则调用此方法将不会触发子对象的布局请求
          *
          * 想要Child被绘制出来,需要调用Child的layout()
          */
        addViewInLayout(child, 0, lp, true);

        //指示在下一层次布局传递期间是否请求此视图的布局
        final boolean needToMeasure = child.isLayoutRequested();

        //对View进行测量
        if (needToMeasure) {
            int childWidthSpec = getChildMeasureSpec(getWidthMeasureSpec(),
                    getPaddingLeft() + getPaddingRight() + lp.leftMargin + lp.rightMargin,
                    lp.width);
            int childHeightSpec = getChildMeasureSpec(getHeightMeasureSpec(),
                    getPaddingTop() + getPaddingBottom() + lp.topMargin + lp.bottomMargin,
                    lp.height);
            child.measure(childWidthSpec, childHeightSpec);
        } else {
            cleanupLayoutState(child);
        }

        int w = child.getMeasuredWidth();
        int h = child.getMeasuredHeight();

        int gravity = lp.gravity;

        //对Gravity的情况进行处理
        if (gravity == -1) {
            gravity = Gravity.TOP | Gravity.START;
        }

        int layoutDirection = getLayoutDirection();
        final int absoluteGravity = Gravity.getAbsoluteGravity(gravity, layoutDirection);
        final int verticalGravity = gravity & Gravity.VERTICAL_GRAVITY_MASK;

        int childLeft;
        int childTop;

        //通过不同的Gravity模式,计算的Card位置
        switch (absoluteGravity & Gravity.HORIZONTAL_GRAVITY_MASK) {
            case Gravity.CENTER_HORIZONTAL:
                childLeft = (getWidth() + getPaddingLeft() - getPaddingRight()  - w) / 2 +
                        lp.leftMargin - lp.rightMargin;
                break;
            case Gravity.END:
                childLeft = getWidth() + getPaddingRight() - w - lp.rightMargin;
                break;
            case Gravity.START:
            default:
                childLeft = getPaddingLeft() + lp.leftMargin;
                break;
        }

        switch (verticalGravity) {
            case Gravity.CENTER_VERTICAL:
                childTop = (getHeight() + getPaddingTop() - getPaddingBottom()  - h) / 2 +
                        lp.topMargin - lp.bottomMargin;
                break;
            case Gravity.BOTTOM:
                childTop = getHeight() - getPaddingBottom() - h - lp.bottomMargin;
                break;
            case Gravity.TOP:
            default:
                childTop = getPaddingTop() + lp.topMargin;
                break;
        }

        //设置Card的位置,不调用此方法addViewInLayout()无效果
        child.layout(childLeft, childTop, childLeft + w, childTop + h);
    }

走到这,我们的这个自定义View通过setAdapter就可以让Adapter中的数据进行显示了。当然这里正常显示还是需要自定义View中的其他代码进行辅助。接下来就让我们进一步展开这个自定义View的其他内容。


setTopView()方法:

设置第一个Card的显示,并且设置监听


private void setTopView() {
        if(getChildCount()>0){
            //拿到第一个Card
            mActiveCard = getChildAt(LAST_OBJECT_IN_STACK);
            if(mActiveCard!=null) {
                //移动Card的监听,具体展开在下边
                flingCardListener = new FlingCardListener(mActiveCard, mAdapter.getItem(0),
                        ROTATION_DEGREES, new FlingCardListener.FlingListener() {

                    @Override
                    public void onCardExited() {
                        mActiveCard = null;
                        mFlingListener.removeFirstObjectInAdapter();
                    }
                    //从左边离开回调
                    @Override
                    public void leftExit(Object dataObject) {
                        mFlingListener.onLeftCardExit(dataObject);
                    }
                    //从右边离开回调
                    @Override
                    public void rightExit(Object dataObject) {
                        mFlingListener.onRightCardExit(dataObject);
                    }
                    //点击回调
                    @Override
                    public void onClick(Object dataObject) {
                        if(mOnItemClickListener!=null)
                            mOnItemClickListener.onItemClicked(0, dataObject);
                    }
                    //手指移动不松开的回调
                    @Override
                    public void onScroll(float scrollProgressPercent) {
                        mFlingListener.onScroll(scrollProgressPercent);
                    }
                });
                mActiveCard.setOnTouchListener(flingCardListener);
            }
        }
    }

OK,到这里setAdapter这条线我们就已经分析出来了。但是这里仅仅是显示多个Card而已,并没有像效果当中的拖动,因此想要拖动的话,势必要存在onTouch事件。而这里我们则需要追溯到FlingCardListener这个类当中,由于篇幅比较长,关于FlingCardListener这个类的展开放到了下一篇博客当中。

本篇下:http://blog.csdn.net/wjzj000/article/details/73441173

尾声

希望各位看官可以star我的GitHub,三叩九拜,满地打滚求star:
https://github.com/zhiaixinyang/PersonalCollect
https://github.com/zhiaixinyang/MyFirstApp

版权声明:本文为wjzj000原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。
本文链接:https://blog.csdn.net/wjzj000/article/details/73432852