最近没事的时候想自己写一个支持下拉刷新,上拉加载的自定义View。写着写着,就觉得最常见的“一个圈转啊转”的进度条太普通了。
于是,就想看看有没有更有趣的一点的加载效果。在GitHub上以”android loading”为关键字一搜索,就发现有作者开源了这么一个库:
库的地址是:https://github.com/ldoublem/LoadingView。里面提供了很多有趣的加载动画(非常棒),个人对其中如下一个效果产生了兴趣:
那么,开源的好处就来了,立刻打开源码瞧一瞧别人是怎么实现的吧。一看发现没有借助任何图片,而就是通过canvas配合属性动画完成的整个效果。
按理说别人造好的轮子,我们直接拿来用就好了。但既然感兴趣,为什么不学习一下别人的思路,自己也来实现一个,从而得到提高呢?
所以,综合一想,自己也重新来画一画这个萌蠢萌蠢的小鬼吧。并通过此文来总结一下整个自定义view的思路和收获。
(P.S:会借鉴原作者的思路,但具体实现细节会有不同,但思路当然才是最重要的,具体实现选择自己喜欢的就好)
其实说起绘画,就想起了小时候流行的一个口诀,是画“丁老头”的,印象中有“一个丁老头儿,借我俩煤球儿,我说三天还,他说四天还..”之类的。
其实就是这样的,如果猛的一下让我们画个“老头儿”出来,我们可能会有点懵逼。但按照口诀那样一部分一部分的画,似乎就变得容易多了。
所以,我们也可以模仿这个思路来画这个小鬼。我们简单分析一下,可以发现这个小鬼的构成其实就是:头 + 眼睛 + 身体 + 影子。
那么,还等什么呢?赶紧按照这个思路“开画”吧!首先,我们当然是新建一个类,并让其继承View,而名字的话就叫GhostView好了。
在正式开始“作画”之前,我们肯定是做好相关的准备工作。比如,先确定好要用多大尺寸的“画纸”。哈哈,其实也就是完成View的measure工作。
我们知道自定义View的时候,如果使用默认的onMeasure()方法:WRAP_CONTENT也会被当做MATCH_PARENT来测量,所以其实要做的也很简单:
// View宽高
private int mWidth, mHeight;
// 默认宽高(WRAP_CONTENT)
private int mDefaultWidth = dip2px(120);
private int mDefaultHeight = dip2px(180);
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
setMeasuredDimension(measureWidth(widthMeasureSpec), measureHeight(heightMeasureSpec));
}
private int measureWidth(int widthMeasureSpec) {
int specMode = MeasureSpec.getMode(widthMeasureSpec);
int specSize = MeasureSpec.getSize(widthMeasureSpec);
if (specMode == MeasureSpec.EXACTLY) {
mWidth = specSize;
} else if (specMode == MeasureSpec.AT_MOST) {
mWidth = Math.min(mDefaultWidth, specSize);
}
return mWidth;
}
private int measureHeight(int heightMeasureSpec) {
int specMode = MeasureSpec.getMode(heightMeasureSpec);
int specSize = MeasureSpec.getSize(heightMeasureSpec);
if (specMode == MeasureSpec.EXACTLY) {
mHeight = specSize;
} else if (specMode == MeasureSpec.AT_MOST) {
mHeight = Math.min(mDefaultHeight, specSize);
}
return mHeight;
}
非常简单,思路就是:
显然,想要画东西,当然我们还需要画笔。可以从之前的效果图里看到,“小鬼”的整个形象需要三种颜色元素,分别是:
白色的身体、黑色的眼睛、以及灰灰的影子。所以,对应来说,我们也需要准备三支不同颜色的画笔:
// 画笔
Paint mBodyPaint, mEyesPaint, mShadowPaint;
private void initPaint() {
mBodyPaint = new Paint();
mBodyPaint.setAntiAlias(true);
mBodyPaint.setStyle(Paint.Style.FILL);
mBodyPaint.setColor(Color.WHITE);
mEyesPaint = new Paint();
mEyesPaint.setAntiAlias(true);
mEyesPaint.setStyle(Paint.Style.FILL);
mEyesPaint.setColor(Color.BLACK);
mShadowPaint = new Paint();
mShadowPaint.setAntiAlias(true);
mShadowPaint.setStyle(Paint.Style.FILL);
mShadowPaint.setColor(Color.argb(60, 0, 0, 0));
}
现在我们的准备工作都做好了,自然就可以正式开始画这个“小鬼”了。我们先从最容易画的入手,搞个圆圆的脑袋出来:
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
drawHead(canvas);
}
// 头部的半径
private int mHeadRadius;
// 圆心(头部)的X坐标
private int mHeadCentreX;
// 圆心(头部)的Y坐标
private int mHeadCentreY;
// 头部最左侧的坐标
private int mHeadLeftX;
// 头部最右侧的坐标
private int mHeadRightX;
// 距离View顶部的内边距
private int mPaddingTop = dip2px(20);
private void drawHead(Canvas canvas) {
mHeadRadius = mWidth / 3;
mHeadCentreX = mWidth / 2;
mHeadCentreY = mWidth / 3 + mPaddingTop;
mHeadLeftX = mHeadCentreX - mHeadRadius;
mHeadRightX = mHeadCentreX + mHeadRadius;
canvas.drawCircle(mHeadCentreX, mHeadCentreY, mHeadRadius, mBodyPaint);
}
代码同样很简单,事实上现在我们就可以使用这个自定义View,来看一看目前为止的效果了:
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android" android:layout_width="match_parent" android:layout_height="match_parent" android:background="@color/colorPrimary">
<me.rawn_hwang.ghostdrawer.Ghost android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_centerInParent="true" android:background="@color/colorAccent" />
</RelativeLayout>
。。。。。。。。好吧,目前为止我们还看不到任何“萌蠢小鬼”的迹象。没关系,一步一步的来。
有了小鬼的头之后,我们接着做什么呢?正常来说我们应该想着接着画身体。但是从之前的效果图我们可以看到,小鬼的底部是有一个影子的。
所以,个人选择先画这个影子。因为:影子位于View的底部,先完成影子的绘画,之后更方面我们确定小鬼身体的高度和位置。
其实所谓的影子也非常的简单,就是一个灰蒙蒙的“椭圆形”而已:
// 影子所占区域
private RectF mRectShadow;
// 小鬼身体和影子之间的举例
private int paddingShadow;
private void drawShadow(Canvas canvas) {
paddingShadow = mHeight / 10;
mRectShadow = new RectF();
mRectShadow.top = mHeight * 8 / 10;
mRectShadow.bottom = mHeight * 9 / 10;
mRectShadow.left = mWidth / 4;
mRectShadow.right = mWidth * 3 / 4;
canvas.drawArc(mRectShadow, 0, 360, false, mShadowPaint);
}
这个时候,我们再来看一看效果变成了什么样子:
现在,我们就来到了最关键的部分了:为小鬼加上身体。其实总的来说,小鬼的身体就是在头部大约半圆的位置,分别画上两条带有弧度的延长线。
但是,怎么才能让小鬼身体的这两条线与头部比较完美的融合呢?原作者在这里使用了一些正弦、余弦的公式来计算圆的弧度,从而完成了需要。
然而,悔不及当初没有好好念书啊,患上了晕“数学公式”的病。所以我机智的选择用另一种方法,虽然没那么高大上,但是比较简单。就像下面这样:
private Path mPath = new Path();
// 小鬼身体胖过头部的宽度
private int mGhostBodyWSpace;
private void drawBody(Canvas canvas) {
mGhostBodyWSpace = mHeadRadius * 2 / 15;
// 先画右边的身体
mPath.moveTo(mHeadLeftX, mHeadCentreY);
mPath.lineTo(mHeadRightX, mHeadCentreY);
mPath.quadTo(mHeadRightX + mGhostBodyWSpace, mRectShadow.top - paddingShadow,
mHeadRightX - mGhostBodyWSpace, mRectShadow.top - paddingShadow);
canvas.drawPath(mPath,mBodyPaint);
}
上图中左边的部分就是我们目前为止得到的效果;而右边就是通过把画笔设置为stroke来解释这样做的原理,实际上就是:先通过lineTo在小鬼头部的中间画一条直径,这个时候path的LastPoint就到了最右边的这个点,然后我们从这个点在右边向下画一条二阶贝塞尔曲线,就有了小鬼右边身体的轮廓了。
那么接着我们该做什么呢?回忆一下,我们发现小鬼的身体下方是有“波纹”的,就想裙子的褶皱一样,所以我们现在就给添上裙子。
其实原理仍然很简单,这个时候path的LastPoint也已经移动到了小鬼右边身体的下面,我们从这里开始向左不断画多个贝塞尔曲线形成裙褶就行了:
// 单个裙褶的宽高
private int mSkirtWidth, mSkirtHeight;
// 裙褶的个数
private int mSkirtCount = 7;
private void drawBody(Canvas canvas) {
mGhostBodyWSpace = mHeadRadius * 2 / 15;
mSkirtWidth = (mHeadRadius * 2 - mGhostBodyWSpace * 2) / mSkirtCount;
mSkirtHeight = mHeight / 16;
// ......
// 从右向左画裙褶
for (int i = 1; i <= mSkirtCount; i++) {
if (i % 2 != 0) {
mPath.quadTo(mHeadRightX - mGhostBodyWSpace - mSkirtWidth * i + (mSkirtWidth / 2), mRectShadow.top - paddingShadow - mSkirtHeight,
mHeadRightX - mGhostBodyWSpace - (mSkirtWidth * i), mRectShadow.top - paddingShadow);
} else {
mPath.quadTo(mHeadRightX - mGhostBodyWSpace - mSkirtWidth * i + (mSkirtWidth / 2), mRectShadow.top - paddingShadow + mSkirtHeight,
mHeadRightX - mGhostBodyWSpace - (mSkirtWidth * i), mRectShadow.top - paddingShadow);
}
}
canvas.drawPath(mPath,mBodyPaint);
}
可以看到到了现在,基本就能看见整个小鬼的轮廓了,但我们注意到小鬼左边似乎有点僵硬。没关系,我们也给他加上一点对应的弧度就行了:
mPath.quadTo(mHeadLeftX - mGhostBodyWSpace, mRectShadow.top - paddingShadow, mHeadLeftX, mHeadCentreY);
到了现在,我们的绘图工作其实基本就已经完成了。但眼睛是心灵的窗户,少了眼睛,这个小鬼看上去有点四不像的感觉。赶紧加上眼睛吧!
同样的,眼睛的绘制其实也非常简单,就在先要的位置,画上两个黑色的小圆就可以了:
private void drawEyes(Canvas canvas) {
canvas.drawCircle(mHeadCentreX , mHeadCentreY, mHeadRadius / 6, mEyesPaint);
canvas.drawCircle(mHeadCentreX + mHeadRadius / 2, mHeadCentreY, mHeadRadius / 6, mEyesPaint);
}
现在我们所有的绘制工作就完成了,把之前粉红色的背景颜色去掉,再看看效果,是不是有点呆萌的赶脚了呢?
现在小鬼我们已经画完了,剩下的工作自然就是让它动起来,别死气沉沉的。而我们已经知道了,这个工作就是通过属性动画来完成的。
那么,我们可以添加一个最简单的位移动画,比如说这样做:
private void startAnim(){
ObjectAnimator animator = ObjectAnimator.ofFloat(this,"translationX",0,500);
animator.setRepeatMode(ObjectAnimator.RESTART);
animator.setRepeatCount(ObjectAnimator.INFINITE);
animator.setDuration(5000);
animator.start();
}
@Override
protected void onDraw(Canvas canvas) {
// ......
startAnim();
}
可以看到这样“小鬼”就已经动起来了,不过现在它肯定没有那么萌了。因为它的行进路径和恐怖片里那些白衣幽灵看上去一样一样的。
不过这里主要是表达个意思嘛,要实现作者原本的那个动画效果实际上也不难,我们分析一下可以发现它主要有几个动作:
就是小鬼在行进的同时还会上下跳动,并且底部的影子会随着小鬼跳起和落下而改变大小,那么我们就可以借助ValueAnimator来实现。
简单来说,要做的工作就是之前描绘小鬼时的相关属性(例如小鬼的头部的圆心坐标,影子的rect的宽度等)不要写死,而是与某个值产生关联。
然后我们用ValueAnimator来监听和不断的改变这个值,然后让view不断重绘,就可以得到响应的一些动画效果了。