效果如上图,笔者在午休的时候,重新追了一遍神探狄仁杰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 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'
}
大家可以自己定义速度(speed)、logo、logo大小、颜色等参数。
github:点我去看看
如果你觉得还不错,动动小手,给作者一个star鼓励鼓励,如果你有想实现的控件愿意和笔者交流的,可以在下面留言,多谢。
作者:橙熟的橙