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

标签: android  github  框架

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


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

写在前面

本来想昨天就把这个效果分析完毕。然后事与愿违,随便玩了玩时间就没了。没办法只有今天补上….最难逃儿女情长书接上回楚霸王,让我们续上仿天天美剧拖动卡片的效果(上):
http://blog.csdn.net/wjzj000/article/details/73432852

开始

我们上部分分析完了setAdapter的一系列的过程,这部分我们要通过FlingCardListener来近距离的看一看我们拖动Card的实现效果。


在开始FlingCardListener之前,让我们回过来上看一个类LinearRegression。可能各位看官已经把这个类给忘了,所以我们在分析的一开始就看一看它。

LinearRegression


    public LinearRegression(float[] x, float[] y) {
        if (x.length != y.length) {
            throw new IllegalArgumentException("array lengths are not equal");
        }
        N = x.length;

        //求平均值
        double sumx = 0.0, sumy = 0.0, sumx2 = 0.0;
        for (int i = 0; i < N; i++) sumx  += x[i];
        for (int i = 0; i < N; i++) sumx2 += x[i]*x[i];
        for (int i = 0; i < N; i++) sumy  += y[i];
        double xbar = sumx / N;
        double ybar = sumy / N;

        //求方差
        double xxbar = 0.0, yybar = 0.0, xybar = 0.0;
        for (int i = 0; i < N; i++) {
            xxbar += (x[i] - xbar) * (x[i] - xbar);
            yybar += (y[i] - ybar) * (y[i] - ybar);
            xybar += (x[i] - xbar) * (y[i] - ybar);
        }
        beta  = xybar / xxbar;
        alpha = ybar - beta * xbar;

        //一些计算公式
        double rss = 0.0;
        double ssr = 0.0;
        for (int i = 0; i < N; i++) {
            double fit = beta*x[i] + alpha;
            rss += (fit - y[i]) * (fit - y[i]);
            ssr += (fit - ybar) * (fit - ybar);
        }

        int degreesOfFreedom = N-2;
        R2    = ssr / yybar;
        svar  = rss / degreesOfFreedom;
        svar1 = svar / xxbar;
        svar0 = svar/N + xbar*xbar*svar1;
    }

FlingCardListener

这里比较重要的方法是onTouch()方法,构造方法传参非常多,各个变量的使用将逐一在onTouch()方法这种展开。因此接下来让我们看一看onTouch()方法。


    @Override
    public boolean onTouch(View view, MotionEvent event) {

        switch (event.getAction() & MotionEvent.ACTION_MASK) {
            case MotionEvent.ACTION_DOWN:
                //确定唯一的坐标点,避免多指操作带来的问题
                mActivePointerId = event.getPointerId(0);
                float x = 0;
                float y = 0;
                boolean success = false;
                try {
                    x = event.getX(mActivePointerId);
                    y = event.getY(mActivePointerId);
                    success = true;
                } catch (IllegalArgumentException e) {
                    Log.w(TAG, "Exception in onTouch(view, event) : " + mActivePointerId, e);
                }
                if (success) {
                    //记住我们按下时的坐标
                    aDownTouchX = x;
                    aDownTouchY = y;
                    //设置View的初始坐标是传入frame的X、Y值
                    if (aPosX == 0) {
                        aPosX = frame.getX();
                    }
                    if (aPosY == 0) {
                        aPosY = frame.getY();
                    }
                    //记录按压位置状态
                    if (y < objectH / 2) {
                        touchPosition = TOUCH_ABOVE;
                    } else {
                        touchPosition = TOUCH_BELOW;
                    }
                }

                view.getParent().requestDisallowInterceptTouchEvent(true);
                break;

            case MotionEvent.ACTION_UP:
                mActivePointerId = INVALID_POINTER_ID;
                //将在下文展开
                resetCardViewOnStack();
                view.getParent().requestDisallowInterceptTouchEvent(false);
                break;

            case MotionEvent.ACTION_MOVE:
                final int pointerIndexMove = event.findPointerIndex(mActivePointerId);
                final float xMove = event.getX(pointerIndexMove);
                final float yMove = event.getY(pointerIndexMove);

                final float dx = xMove - aDownTouchX;
                final float dy = yMove - aDownTouchY;

                aPosX += dx;
                aPosY += dy;
                /**
                 * objectX= frame.getX()
                 * 此处为计算移动距离
                  */
                float distobjectX = aPosX - objectX;
                /**
                 * 如果我们仔细看Card的拖动效果可以发现,
                 * 有一个小小的旋转效果如果我们仔细看Card的拖动效果可以发现,有一个小小的旋转效果
                 *
                 * 此处是计算旋转角度自定义的公式,当拖动X轴距离和parentWidth相等时,
                 * 那么最大旋转的效果就是:BASE_ROTATION_DEGREES * 2.f
                 */
                float rotation = BASE_ROTATION_DEGREES * 2.f * distobjectX / parentWidth;
                if (touchPosition == TOUCH_BELOW) {
                    rotation = -rotation;
                }

                //设置Card的位置,旋转等
                frame.setX(aPosX);
                frame.setY(aPosY);
                frame.setRotation(rotation);
                //onScroll方法调用
                mFlingListener.onScroll(getScrollProgressPercent());
                break;
        }

        return true;
    }

这里的思路是通过setListener的时候,new FlingCardListener把每一个Card的View对象和Adapter中对应的数据,FlingCardListener将通过onTouch()方法中的坐标变换进行对Card
位置进行设置,并且完成回调。


resetCardViewOnStack()


    private boolean resetCardViewOnStack() {
        //如果超出左边界限
        if (movedBeyondLeftBorder()) {
            // 执行左划出效果,相关执行代码在这个方法中( onSelected()在下文展开 )
            // getExitPoint()得到离开时的坐标Y的方法
            onSelected(true, getExitPoint(-objectW), 100);
            mFlingListener.onScroll(-1.0f);
        } else if (movedBeyondRightBorder()) {
            // 执行右划出效果
            onSelected(false, getExitPoint(parentWidth), 100);
            mFlingListener.onScroll(1.0f);
        } else {
            //如果不从左或右划出,那么便是Card回弹
            float abslMoveDistance = Math.abs(aPosX - objectX);
            aPosX = 0;
            aPosY = 0;
            aDownTouchX = 0;
            aDownTouchY = 0;
            frame.animate()
                    .setDuration(200)
                    .setInterpolator(new OvershootInterpolator(1.5f))
                    .x(objectX)
                    .y(objectY)
                    .rotation(0);
            mFlingListener.onScroll(0.0f);
            if (abslMoveDistance < 4.0) {
                mFlingListener.onClick(dataObject);
            }
        }
        return false;
    }

onSelected()

Card划出的代码实现,需要接受exitY,离开的坐标Y


    public void onSelected(final boolean isLeft,float exitY, long duration) {
        isAnimationRunning = true;
        float exitX;
        if (isLeft) {
            /**
             * 计算离开点的X坐标(左移动为减,右移动为加)
             * getRotationWidthOffset():
             *         objectW / MAX_COS - objectW(计算旋转的宽度偏移量)
             *         MAX_COS=cos45
             */
            exitX = -objectW - getRotationWidthOffset();
        } else {
            exitX = parentWidth + getRotationWidthOffset();
        }

        //弹出Card的代码实现
        this.frame.animate()
                .setDuration(duration)
                .setInterpolator(new AccelerateInterpolator())
                .x(exitX)
                .y(exitY)
                .setListener(new AnimatorListenerAdapter() {
                    @Override
                    public void onAnimationEnd(Animator animation) {
                        if (isLeft) {
                            //左边移出回调
                            mFlingListener.onCardExited();
                            mFlingListener.leftExit(dataObject);
                        } else {
                            //右边移出回调
                            mFlingListener.onCardExited();
                            mFlingListener.rightExit(dataObject);
                        }
                        isAnimationRunning = false;
                    }
                })
                .rotation(getExitRotation(isLeft));
    }

getExitPoint()

得到离开时的坐标Y的方法


private float getExitPoint(int exitXPoint) {
        float[] x = new float[2];
        x[0] = objectX;
        x[1] = aPosX;

        float[] y = new float[2];
        y[0] = objectY;
        y[1] = aPosY;

        LinearRegression regression = new LinearRegression(x, y);

        //这个返回值是一个线性方程: y = ax+b
        return (float) regression.slope() * exitXPoint + (float) regression.intercept();
    }

梳理

走到这里,整体的流程其实还是比较明确的:我们折叠的多个Card的效果是通过类似ListView的实现就行完成的。每一个Card的View在通过设置Listener进行完成Card的左右拖动的效果。而实现弹动的效果是用属性动画完成的。


尾声

希望各位看官可以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/73441173