仿链家splash

因为最近购房使用链家APP较多,无意中发现链家的splash挺不错,刚好这几天赋闲在家(公司出游,自己要办贷款手续没法去),就模仿着写了一下,分享给大家:

image

因为色值、图形、素材以及动画效果等并没有刻意的去模仿,外加鄙人艺术细胞不足,所以最终效果有点差强人意。虽然有点丑,但基本的效果都有,不影响我们探讨其实现过程:

一、动画过程整体分析

仔细观察这个页面,发现整个过程包含三个部分,
  1. 整体背景放大
  2. 中间icon的绘制
  3. 底部文字alpha变化
其中,1和3实现比较容易,而且关联性较小,属性动画即可实现,因此关键点在于2的实现。

二、icon绘制过程分析

为了便于技术实现,我们将icon的绘制过程由四个步骤组成:
  • 第一步:水滴滴落。水滴从上而下,到底部后弹起一定高度,然后再次滴落。
  • 第二步:房子icon绘制。房子的绘制从底部开始,到顶部,然后到右侧(由部分空缺)。
  • 第三步:小圆点晃动。房子右侧的小圆点上下晃动,最后回到空缺的中部。
  • 第四步:完整图形。动画完成后的形状。

三、icon绘制的实现

为了清晰的表达动画的各个过程首先定义了一个枚举类型
public static enum State {
    END,//结束状态
    WATER_DROP,//水滴滴落
    HOUSE_DRAW,//房子绘制
    CIRCLE_SHAKE//中心小圆点晃动
}
其实,可以在这个枚举里加入一个draw(canvas)方法,这样绘制的过程,直接通过当前的state的draw方法来完成,逻辑可能更加的清晰。但很多人不太习惯这样的写法,而且也有可能降低我们这个enum 的可读性。
@Override
protected void onSizeChanged(int w, int h, int oldw, int oldh) {
    super.onSizeChanged(w, h, oldw, oldh);
    updateBySize(w, h, oldw, oldh);
}

private void updateBySize(int w, int h, int oldw, int oldh) {
    mViewHeight = h;
    mViewWidth = w;
    int small = mViewHeight < mLineWidth ? mViewHeight : mViewWidth;
    mLineWidth = small / mLineWidthRate;
    mPaint.setStrokeWidth(mLineWidth);
    mLitleCircleR = small / mLitleCircleRRate;
    mDropCircleR = small / mDropCircleRRate;
    mDropOutsidePoint = small / mDropOutsidePointRate;
    mHouseWidth = small / mHouseWidthRate;
    mDropDistance = small / mDropDistanceRate;
    buidHousePath();
}


/**
 * 构造house路径
 */
private void buidHousePath() {
    mHousePath.moveTo((int) (mHouseWidth * 0.5), mViewHeight - mLineWidth);//初始点
    mHousePath.lineTo(-(int) (mHouseWidth * 0.5), mViewHeight - mLineWidth);
    mHousePath.lineTo(-(int) (mHouseWidth * 0.5), (int) (mViewHeight - mLineWidth - mHouseWidth * 0.8));
    mHousePath.lineTo(0, (int) (mViewHeight - mLineWidth - mHouseWidth * 1.2));
    mHousePath.lineTo((int) (mHouseWidth * 0.5), (int) (mViewHeight - mLineWidth - mHouseWidth * 0.8));
    mHousePath.lineTo((int) (mHouseWidth * 0.5), (int) (mViewHeight - mLineWidth - mHouseWidth * 0.6));
    mMeasure.setPath(mHousePath, false);
}
在onSizeChanged获得view的宽高,通过两边的最小值计算出线宽,圆半径,移动距离等值(这些值与view的两边最小值设定一定的比例),并构建出房子图形的路径。仅仅是为了演示实现过程,所以并没有使用在attr中自定义属性方式对view进行设定,建议大家自行完善,关于attr的使用,后期有机会再给大家分享。
接着进行一些初始化操作:主要包括画笔,动画,以及监听
private void init(Context context) {
        this.mContext = context;
        mPaint = new Paint();
        mPaint.setColor(Color.GREEN);
        mPaint.setStrokeCap(Paint.Cap.ROUND);
        mPaint.setStrokeJoin(Paint.Join.ROUND);//圆弧
        mPaint.setStyle(Paint.Style.FILL);
        mPaint.setAntiAlias(true);
        mPaint.setDither(true);
        mDropPath = new Path();
        mHousePath = new Path();
        mHouseTmpPath = new Path();
        mMeasure = new PathMeasure();
        initListener();
        initAnimator();
    }
/**
     * 初始化动画
     */
    private void initAnimator() {
        //初始动画
        mDropCircleAnimator = ValueAnimator.ofFloat(0, 1).setDuration(mDefaultDuration);
        mHouseDrowAnimator = ValueAnimator.ofFloat(0, 1).setDuration(mDefaultDuration);
        mCircleShakeAnimator = ValueAnimator.ofFloat(0, 1).setDuration(mDefaultDuration);
        //设置插值器
        mDropCircleAnimator.setInterpolator(new DropInterpolator());
        mHouseDrowAnimator.setInterpolator(new DecelerateInterpolator());
        //设置进度监听
        mDropCircleAnimator.addUpdateListener(mAnimatorUpdateListener);
        mHouseDrowAnimator.addUpdateListener(mAnimatorUpdateListener);
        mCircleShakeAnimator.addUpdateListener(mAnimatorUpdateListener);
        //设置动画监听
        mDropCircleAnimator.addListener(mAnimatorListener);
        mHouseDrowAnimator.addListener(mAnimatorListener);
        mCircleShakeAnimator.addListener(mAnimatorListener);
    }
使用了属性动画ValueAnimator,并根据不同效果设定了不同的插值器。然而Android提供的插值器不能完全满足我们的效果,需要进行一定的定制。项目中我们通过两种方式来完成:
第一种使用自定义Interpolator(水滴下落):
class DropInterpolator implements TimeInterpolator {

        @Override
        public float getInterpolation(float input) {
            if (input <= 0.6) {
                return input / 0.6f;
            } else if (input <= 0.8) {
                return 1 - ((input - 0.6f) / 0.2f) * 0.4f;
            } else {
                return 0.6f + ((input - 0.8f) / 0.2f) * 0.4f;
            }


        }
    }
第二种在基础Interpolator上进行二次计算,也就是通过提供的Interpolator返回值进行二次计算:
/**
     * 获取Y 坐标
     *
     * @return
     */
    private float getLitleCicleY() {
        if (mAnimatorValue <= 0.5) {
            return mViewHeight - mLineWidth - mHouseWidth * 0.6f + mAnimatorValue / 0.5f *                      mHouseWidth * 0.3f;
        } else if (mAnimatorValue <= 0.75) {
            return mViewHeight - mLineWidth - mHouseWidth * 0.3f - (mAnimatorValue - 0.5f) / 0.25f *         mHouseWidth * 0.1f;
        } else {
            return mViewHeight - mLineWidth - mHouseWidth * 0.4f + (mAnimatorValue - 0.75f) / 0.25f * mHouseWidth * 0.1f;
        }
    }
动画的监听分为进度和状态两种:
private void initListener() {
        mAnimatorUpdateListener = new ValueAnimator.AnimatorUpdateListener() {
            @Override
            public void onAnimationUpdate(ValueAnimator animation) {
                mAnimatorValue = (float) animation.getAnimatedValue();
                invalidate();
            }
        };
进度监听主要是为了获得每个动画执行的百分比,并调用invalidate()进行重绘。
mAnimatorListener = new Animator.AnimatorListener() {
            @Override
            public void onAnimationStart(Animator animation) {

            }

            @Override
            public void onAnimationEnd(Animator animation) {
                changeAnimationState();
            }

            @Override
            public void onAnimationCancel(Animator animation) {

            }

            @Override
            public void onAnimationRepeat(Animator animation) {

            }
        };
    }
状态监听主要是针对不同的状态进行不同的操作,我们这里主要是在动画每个动画结束时调用 changeAnimationState()进行下一个动画的启动以及画笔相关处理操作。
/***
     * 更改动画状态
     */
    private void changeAnimationState() {
        switch (mCurrentState) {
            case WATER_DROP:
                mCurrentState = State.HOUSE_DRAW;
                mPaint.setStrokeWidth(mLineWidth);
                mPaint.setStyle(Paint.Style.STROKE);
                mHouseDrowAnimator.start();
                break;
            case HOUSE_DRAW:
                mCurrentState = State.CIRCLE_SHAKE;
                mCircleShakeAnimator.start();
                break;
            case CIRCLE_SHAKE:
                mPaint.setStyle(Paint.Style.STROKE);
                mCurrentState = State.END;
                if (null != mListener) {
                    mListener.onEnd();
                }
                break;
        }

        if (null != mListener) {
            mListener.onStateChange(mCurrentState);
        }
    }


/***
     * 动画回调
     */
    interface AnimationListener {
        void onStart();

        void onEnd();

        void onStateChange(State state);
    }
changeAnimationState()方法里除了画笔以及动画顺序的控制之外,还可以通过mListener回调给调用者整个房子icon动画的过程和状态。
@Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);
        canvas.translate(mViewWidth / 2, 0);
        switch (mCurrentState) {
            case WATER_DROP:
                drawDrop(canvas);
                break;
            case HOUSE_DRAW:
                drawHouse(canvas);
                break;
            case CIRCLE_SHAKE:
                drawCircle(canvas);
                break;
            case END:
                drawEnd(canvas);
                break;
        }

    }
onDraw(canvas)中根据不同的状态进行不同的动画绘制
/**
     * 画水滴动画
     *
     * @param canvas
     */
    private void drawDrop(Canvas canvas) {
        mDropPath.reset();
        RectF oval = new RectF((int) ((mHouseWidth * 0.5) - mDropCircleR * (1 - mAnimatorValue)),
                (int) (mDropDistance * mAnimatorValue),
                (int) (mDropCircleR * (1 - mAnimatorValue) + (mHouseWidth * 0.5)),
                (int) (mDropCircleR * (1 - mAnimatorValue) * 2 + mDropDistance * mAnimatorValue));
        mDropPath.addArc(oval, -180, 180);
        mDropPath.lineTo((int) (mHouseWidth * 0.5), mDropCircleR * (1 - mAnimatorValue) +
                mDropDistance * mAnimatorValue + mDropOutsidePoint * (1 - mAnimatorValue));
        canvas.drawPath(mDropPath, mPaint);
    }
水滴下落动画的关键点在于水滴形状的绘制,这里偷了个懒,没用贝塞尔曲线,而是直接搞了个半圆,以及两条切线。
/***
     * 画房子动画
     * @param canvas
     */
    private void drawHouse(Canvas canvas) {
        mMeasure.getSegment(0, mMeasure.getLength() * mAnimatorValue, mHouseTmpPath, true);
        canvas.drawPath(mHouseTmpPath, mPaint);

    }
房子动画的核心在于PathMeasure的使用,通过其getSegment可以获得其一部分路径,PathMeasure的用处很多,大家可以抽空研究一下,如果有机会我也会给大家分享一下
/**
     * 画小圆动画
     *
     * @param canvas
     */
    private void drawCircle(Canvas canvas) {
        mPaint.setStyle(Paint.Style.STROKE);
        canvas.drawPath(mHousePath, mPaint);
        mPaint.setStyle(Paint.Style.FILL);
        canvas.drawCircle(mHouseWidth * 0.5f, getLitleCicleY(), mLitleCircleR, mPaint);
    }
小圆晃动的动画相对比较简单,这里没有使用自定义插值器,而是自己在插值器数据上进行二次计算来完成。可能不太容易理解,但其跟自定义插值器的原理是一致的。
/**
     * 绘画结束状态
     *
     * @param canvas
     */
    private void drawEnd(Canvas canvas) {
        if (isShow) {
            mPaint.setStyle(Paint.Style.STROKE);
            canvas.drawPath(mHousePath, mPaint);
            mPaint.setStyle(Paint.Style.FILL);
            canvas.drawCircle(mHouseWidth * 0.5f, mViewHeight - mLineWidth - mHouseWidth * 0.3f, mLitleCircleR, mPaint);
        }
    }
绘制结束状态就是把房子和小圆点绘制上即可,没有动画效果,isShow这个主要是用来判断这个状态是否要显示用的。

需要注意的是,使用这个view时要关闭硬件加速,要不然动画效果出不来。因为调用View.invalidate()的话,缓存的层会不得不重新渲染。

项目地址:Github

猜你喜欢

转载自blog.csdn.net/m0_37041332/article/details/80022088