因为最近购房使用链家APP较多,无意中发现链家的splash挺不错,刚好这几天赋闲在家(公司出游,自己要办贷款手续没法去),就模仿着写了一下,分享给大家:
因为色值、图形、素材以及动画效果等并没有刻意的去模仿,外加鄙人艺术细胞不足,所以最终效果有点差强人意。虽然有点丑,但基本的效果都有,不影响我们探讨其实现过程:
一、动画过程整体分析
仔细观察这个页面,发现整个过程包含三个部分,
- 整体背景放大
- 中间icon的绘制
- 底部文字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);
}
}