在Android上画一个月食动画

貌似7月28号有月食,我个人对天文现象还是挺感兴趣的,虽然已经过了一周了,但还是想把它留下来。
之前看到一个H5的月食动画,感觉很有意思,于是拿起Android的画笔画布,自己实现了一个,很简单,但看起来还不错。
先看看效果:
月食
其中黄色的我们就当作月亮吧,黑色的当作地球或者别的星体,白底就当作天空吧哈哈。
为了好玩一点,这个动画除了能自动播放,还能点击暂停和支持拖动地球去随意遮挡月亮。
其实核心算法就一句话:随两圆圆心的距离改变而改变背景灰度。
我们可以注意到,天空变黑时是从两圆相切时开始的。

来来来,我们先自定义一个View,声明一堆必要的常量和变量,再初始化一下我们的画笔:

public class EclipseView extends View {

    private static final String MOON_COLOR = "#FFEB3B"; // 月亮的颜色代码
    private static final String EARTH_COLOR = "#000000"; // 地球的颜色代码
    private static final int RADIUS = 192; // 月亮半径,随便给一个
    private static final float MOVE_SPEED = 0.02f; // 移动速度因子

    private Paint mMoonPaint; // 月亮画笔
    private Paint mEarthPaint; // 地球画笔
    private PointF mEarthPoint = new PointF(RADIUS, RADIUS); // 地球的可移动圆心

    private int mWidth = 0; // View的宽度
    private int mHeight = 0; // View的高度
    private int mSkyRGB = 255; // 天空的RGB单值
    private float mDegree = 0; // 地球旋转角度
    private boolean mIsTouchMode = false; // 是否为手动拖动模式

    public EclipseView(Context context, AttributeSet attrs) {
        super(context, attrs);
        init();
    }

    private void init() {
        mMoonPaint = new Paint();
        mMoonPaint.setColor(Color.parseColor(MOON_COLOR)); // 给画笔上色了
        mMoonPaint.setAntiAlias(true); // 抗锯齿!
        mMoonPaint.setDither(true); // 防抖动!

        mEarthPaint = new Paint();
        mEarthPaint.setColor(Color.parseColor(EARTH_COLOR));
        mEarthPaint.setAntiAlias(true);
        mEarthPaint.setDither(true);
    }
}

先把最简单的天空画了,就是涂涂颜色。这里可能要大家了解一下RGB的原理,当R,G,B三个值互相相等时,就是灰度图了,像小时候看的黑白电视,从白色(255, 255, 255)到黑色(0, 0, 0),我们会看到一个由白逐渐变灰最后变成黑色的过程。
等会儿我们动态改变mSkyRGB变量,就能改变天空的颜色了!

    private void drawSky(Canvas canvas) {
        canvas.drawColor(getSkyColorInt());
    }

    private int getSkyColorInt() {
        return Color.rgb(mSkyRGB, mSkyRGB, mSkyRGB);
    }

月亮是固定的,也比较好画。我们要画一个圆,自然要有坐标,半径,然后和一支笔。
宽高各除以2,就能把月亮摆在中间了:

    private void drawMoon(Canvas canvas) {
        canvas.drawCircle(mWidth / 2, mHeight / 2, RADIUS, mMoonPaint);
    }

再来画旋转的地球,为了画地球的运动轨迹(是一个半径为地球半径两倍的大圆,当然你也可以设为3倍4倍都行),我们要知道半径为1的圆方程为sin(x)^2 + cos(x)^2 = 1,由此我们可以得到圆周上点的坐标为(R * cos(x), R * sin(x)),R为半径。
示意图
在Android当中,画布的坐标原点在左上角,向右为x轴正方向,向下为y轴正方向。因此我们能得出:
A点坐标为(mWidth/2, mHeight/2),B点坐标为(mWidth/2, mHeight/2 - 2 * RADIUS),AB距离即月亮或地球的两倍半径。C点即地球的动态圆心,也就是下面代码中的mEarthPoint,很显然就是以B为旋转中心来计算了,C和B保持水平时是起始点,顺时针旋转,所以C的坐标表示为x = mWidth/2 + 2 * RADIUS * cos(mDegree), y = mHeight/2 - 2 * RADIUS + 2 * RADIUS * sin(mDegree)

    private void drawEarth(Canvas canvas) {
        if (!mIsTouchMode) {
            if (mDegree > 360) {
                mDegree = 0; // 当转满一圈时重置角度
            }
            mEarthPoint.x = (float) (Math.cos(mDegree) * RADIUS * 2 + mWidth / 2);
            mEarthPoint.y = (float) (Math.sin(mDegree) * RADIUS * 2 + mHeight / 2 - RADIUS * 2);
            mDegree += MOVE_SPEED; // 每次绘制加一点角度,增量越少画得越慢
            calculateSkyRGB(); // 动态计算天空颜色
            postInvalidate(); // 角度改变,需要刷新画布,这样就能循环绘制了
        }
        // 圆周运动画完了该画地球本身了,这里画地球的时候,半径稍微减去一点,这样就会比月亮小一细圈,到时候月食时重合边缘就会有余光,比较好看
        canvas.drawCircle(mEarthPoint.x, mEarthPoint.y, RADIUS - 2, mEarthPaint);
    }

当地球开始覆盖月亮时,覆盖越多,天空就会越黑,量化这个变化就是计算两圆圆心的距离。

    private void calculateSkyRGB() {
        double totalOffset = RADIUS * 2; // 从两圆相切开始变化,所以起始距离就是2倍半径啦!此时天空是最亮的
        double nowOffset = Math.sqrt(Math.pow(mWidth / 2 - mEarthPoint.x, 2)
                + Math.pow(mHeight / 2 - mEarthPoint.y, 2)); // 距离公式求两点间实时距离
        mSkyRGB = (int) (255 * nowOffset / totalOffset); // 用实时距离除以相切时的初始距离就能得到一个天空颜色变化的比例,255是白色的RGB单值,离得越近,值就越小,也就越黑
        if (mSkyRGB > 255) // 当地球远离月亮时,实时距离就会超过相切距离了,而RGB值是不能超过255的,所以这里要处理一下避免越界
            mSkyRGB = 255;
        else if (mSkyRGB < 24) // 这里处理的原因是当天空颜色快接近纯黑(RGB单值=0)时,会和地球的颜色一致(因为地球也是黑色),为了让地球完全覆盖月亮时天空不要太黑,所以设置一个下限24
            mSkyRGB = 24;
    }

其实到这里我们基本上已经完成整个动画逻辑了,只要重写一下onDraw,把该画的画上去,就OK了。

    @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);
        mWidth = getMeasuredWidth();
        mHeight = getMeasuredHeight();
        drawSky(canvas);
        drawMoon(canvas);
        drawEarth(canvas);
    }

最后,我们为了可以用手指拖动地球,自然就要重写一下触摸事件:

    @Override
    public boolean onTouchEvent(MotionEvent event) {
        switch (event.getAction()) {
            case MotionEvent.ACTION_DOWN: // 按下
                break;
            case MotionEvent.ACTION_MOVE: // 滑动
                mEarthPoint.x = event.getX(); // 让地球的圆心随手指动态改变坐标
                mEarthPoint.y = event.getY();
                calculateSkyRGB(); // 记得重新计算天空颜色
                postInvalidate(); // 坐标改变,需要刷新画布
                break;
            case MotionEvent.ACTION_UP: // 抬起
                mIsTouchMode = !mIsTouchMode; // 切换模式,控制绘制地球时是否移动
                postInvalidate();
                break;
        }
        return true;
    }

虽然这个动画很简单,但把自定义绘图的一些基本知识都用上了。对于各位大神来说,自然是小儿科了。
源码地址:https://github.com/ysy950803/LunarEclipse/tree/master/demo/src/main/java/com/ysy/lunareclipse/demo

  • 3
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值