一个仿日食的自定义view

Xenia ·
更新时间:2024-11-10
· 861 次阅读

一、写在前面的话

效果如上图,笔者在午休的时候,重新追了一遍神探狄仁杰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鼓励鼓励,如果你有想实现的控件愿意和笔者交流的,可以在下面留言,多谢。


作者:橙熟的橙



日食 view 自定义view

需要 登录 后方可回复, 如果你还没有账号请 注册新账号