用Canvas和属性动画造一只萌蠢的“小鬼”

最近没事的时候想自己写一个支持下拉刷新,上拉加载的自定义View。写着写着,就觉得最常见的“一个圈转啊转”的进度条太普通了。
于是,就想看看有没有更有趣的一点的加载效果。在GitHub上以”android loading”为关键字一搜索,就发现有作者开源了这么一个库:

这里写图片描述

库的地址是:https://github.com/ldoublem/LoadingView。里面提供了很多有趣的加载动画(非常棒),个人对其中如下一个效果产生了兴趣:

这里写图片描述

那么,开源的好处就来了,立刻打开源码瞧一瞧别人是怎么实现的吧。一看发现没有借助任何图片,而就是通过canvas配合属性动画完成的整个效果。
按理说别人造好的轮子,我们直接拿来用就好了。但既然感兴趣,为什么不学习一下别人的思路,自己也来实现一个,从而得到提高呢?
所以,综合一想,自己也重新来画一画这个萌蠢萌蠢的小鬼吧。并通过此文来总结一下整个自定义view的思路和收获。
(P.S:会借鉴原作者的思路,但具体实现细节会有不同,但思路当然才是最重要的,具体实现选择自己喜欢的就好)


自定义View的建立

其实说起绘画,就想起了小时候流行的一个口诀,是画“丁老头”的,印象中有“一个丁老头儿,借我俩煤球儿,我说三天还,他说四天还..”之类的。
其实就是这样的,如果猛的一下让我们画个“老头儿”出来,我们可能会有点懵逼。但按照口诀那样一部分一部分的画,似乎就变得容易多了。

所以,我们也可以模仿这个思路来画这个小鬼。我们简单分析一下,可以发现这个小鬼的构成其实就是:头 + 眼睛 + 身体 + 影子
那么,还等什么呢?赶紧按照这个思路“开画”吧!首先,我们当然是新建一个类,并让其继承View,而名字的话就叫GhostView好了。


onMeasure()

在正式开始“作画”之前,我们肯定是做好相关的准备工作。比如,先确定好要用多大尺寸的“画纸”。哈哈,其实也就是完成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;
    }

非常简单,思路就是:

  • 当View的宽高指定为MATCH_PARENT或者明确的值的时候,就使用实际的值。
  • 当View的宽高指定为WRAP_CONTENT时,宽度为默认的120dp,高度则为180dp。

Paint,准备画笔

显然,想要画东西,当然我们还需要画笔。可以从之前的效果图里看到,“小鬼”的整个形象需要三种颜色元素,分别是:
白色的身体、黑色的眼睛、以及灰灰的影子。所以,对应来说,我们也需要准备三支不同颜色的画笔:

    // 画笔
    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不断重绘,就可以得到响应的一些动画效果了。

  • 10
    点赞
  • 15
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值