一個仿日食的自定義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鼓勵鼓勵,如果你有想實現的控件願意和筆者交流的,可以在下面留言,多謝。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章