一个仿日食的自定义view

一、写在前面的话

效果如上图,笔者在午休的时候,重新追了一遍神探狄仁杰II,蛇灵密谋利用日食,引洛河之水颠覆武周社稷。日食来临时,天地昏暗,日食之后万物回复光明。看完一想,要不我也搞一个日食效果看看,于是,就有了这篇文章。

二、分析动画

首先有一个圆,取个名字,叫太阳(Sun),月亮(Moon)缓缓从Sun上滑过,并且随着两个圆重合,背景颜色逐渐变深,在完全重合的时候深度达到最大,然后Moon缓缓与Sun分离,分离的时候背景颜色逐渐变浅,最后恢复原状,logo出现。这里有两个技术点,第一是背景色的控制;第二就是Moon这个圆只有与Sun重合的部分才可见,其余部分不可见,这样的日食才是逼真的。

三、作画(onDraw)

//draw太阳
        canvas?.drawCircle(centerX.toFloat(), centerY.toFloat(), mSunR.toFloat(), mSunPaint)

首先画一个圆,作为太阳。那么月亮是不是按照同样的方式画一个比较小的圆?其实不是的,如果按照同样的方式,画出的圆回覆盖在月亮上,真正的日食因为太阳的亮度很大,通过肉眼只能在太阳上看到月亮,所以我们要用到PorterDuffXfermode这个东西,如下所示:

        xfermode = PorterDuffXfermode(PorterDuff.Mode.SRC_ATOP)

这个的mode有很多种,这个不是本文的重点,这里不做介绍,大概的意思就是只有重合的部分可见。画月亮的部分如下所示


        //draw月亮
        mSunPaint.color = mMoonColor

        mSunPaint.xfermode = xfermode
        canvas?.drawCircle(mMoonX.toFloat(), centerY.toFloat(), mMoonR.toFloat(), mSunPaint)
        mSunPaint.xfermode = null

接下来画logo,要在Sun和Moon第一次重合后才画,并且要在画月亮之前。所以onDraw部分完整的代码如下

override fun onDraw(canvas: Canvas?) {
        super.onDraw(canvas)
        mSunPaint.color = mSunColor
        //draw太阳
        canvas?.drawCircle(centerX.toFloat(), centerY.toFloat(), mSunR.toFloat(), mSunPaint)

        if (isLogoShow) {
            mLogoPaint.xfermode = xfermode
            canvas?.drawBitmap(mLogoBitmap, mLogoBitmapSrcRect, mLogoBitmapDesRect, mLogoPaint)
            mLogoPaint.xfermode = null
        }
        //draw月亮
        mSunPaint.color = mMoonColor

        mSunPaint.xfermode = xfermode
        canvas?.drawCircle(mMoonX.toFloat(), centerY.toFloat(), mMoonR.toFloat(), mSunPaint)
        mSunPaint.xfermode = null

    }

元素都画好了,接下里就是让他们动起来,我这里用的是handler实现的。首先是进入的动画,每一次改变Moon的X坐标

 MSG_IN -> {
                if (mMoonX < centerX) {
                    mMoonX += mMoonXOffset
                    invalidate()
                    it.target.sendEmptyMessageDelayed(MSG_IN, mAnimationSpeed)
                } else if (kotlin.math.abs(mMoonX - centerX) <= mMoonXOffset) {
                    isLogoShow = true
                    invalidate()
                    it.target.sendEmptyMessageDelayed(MSG_OUT, 500)
                }
                progressOffset = txfloat((mMoonX - mMoonStartX), (4 * mSunR))

            }

退出动画也和进入动画一样,做不过要画上logo,画笔的透明度逐渐变大,这样logo出现的不会太突兀。

MSG_OUT -> {
                if (mMoonX - centerX < 2 * mSunR) {
                    mMoonX += mMoonXOffset
                    var tempAlpha = 255 * (mMoonX - centerX) / (2 * mSunR)
                    if (tempAlpha > mLogoPaint.alpha) {
                        if (tempAlpha > 255) {
                            tempAlpha = 255
                        }
                        mLogoPaint.alpha = tempAlpha
                    }
                    invalidate()
                    it.target.sendEmptyMessageDelayed(MSG_OUT, mAnimationSpeed)
                }
                progressOffset = txfloat((mMoonX - mMoonStartX), (4 * mSunR))

            }

接下来就是背景,这里的颜色的变化我给大家提供一个方法计算颜色。

/**
     * 根据fraction值来计算当前的颜色。
     */
    private fun getCurrentColor(fraction: Float, startColor: Int, endColor: Int): Int {
        val redCurrent: Int
        val blueCurrent: Int
        val greenCurrent: Int
        val alphaCurrent: Int
        val redStart = Color.red(startColor)
        val blueStart = Color.blue(startColor)
        val greenStart = Color.green(startColor)
        val alphaStart = Color.alpha(startColor)
        val redEnd = Color.red(endColor)
        val blueEnd = Color.blue(endColor)
        val greenEnd = Color.green(endColor)
        val alphaEnd = Color.alpha(endColor)
        val redDifference = redEnd - redStart
        val blueDifference = blueEnd - blueStart
        val greenDifference = greenEnd - greenStart
        val alphaDifference = alphaEnd - alphaStart
        redCurrent = (redStart + fraction * redDifference).toInt()
        blueCurrent = (blueStart + fraction * blueDifference).toInt()
        greenCurrent = (greenStart + fraction * greenDifference).toInt()
        alphaCurrent = (alphaStart + fraction * alphaDifference).toInt()
        return Color.argb(alphaCurrent, redCurrent, greenCurrent, blueCurrent)
    }

我们背景需要通过监听器回调出去,让view的父布局去实现。为什么呢?因为如果我们在控件内部画了背景,那么通过xfermode方法画的Moon就在整个背景上可见(我们要求日食Moon只能在与Sun重合的部分才可见)

   if (it.what == MSG_IN || it.what == MSG_OUT) {
            val color = if (progressOffset * 2 > 1) {
                getCurrentColor(2 - progressOffset * 2, mBgStartColor, mBgEndColor)
            } else {
                getCurrentColor(progressOffset * 2, mBgStartColor, mBgEndColor)
            }
            eclipseListener?.onColor(color)
        }

这样,我们日食就完成了。

四、使用

Add it in your root build.gradle at the end of repositories:

	allprojects {
		repositories {
			...
			maven { url 'https://jitpack.io' }
		}
	}

Add the dependency

	dependencies {
	        implementation 'com.github.cqcby1994:MyView:1.2.1'
	}
<com.chen.eclipseview.EclipseLogo
        android:id="@+id/eclipse_view"
        app:logoSize="large"
        app:speed="middle"
        app:sunColor="@android:color/white"
        app:moonColor="#279536"
        app:endBackgroundColor="#279536"
        app:startBackgroundColor="@android:color/white"
        app:logoSrc="@mipmap/wechat"
        android:layout_width="200dp"
        android:layout_height="200dp"
 />

 大家可以自己定义速度(speed)、logo、logo大小、颜色等参数。

github:点我去看看

如果你觉得还不错,动动小手,给作者一个star鼓励鼓励,如果你有想实现的控件愿意和笔者交流的,可以在下面留言,多谢。

发布了15 篇原创文章 · 获赞 10 · 访问量 1万+

猜你喜欢

转载自blog.csdn.net/qwe749082787/article/details/105372683