Android:使用drawBitmapMesh方法產生水波

一、認識Canvas.drawBitmapMesh

Mesh的含義是“網格”,也就是說它將整個Bitmap分成若干個網格,再對每一個網格進行相應的扭曲處理。至于其具體是怎么運作的,我們邊做邊說。

1.1 創建一個View

class RippleView : View {
    constructor(context: Context?) : super(context)
    constructor(context: Context?, attrs: AttributeSet?) : super(context, attrs)
    constructor(context: Context?, attrs: AttributeSet?, defStyleAttr: Int) : super(context, attrs, defStyleAttr)
}

當View創建完成后,我們將其添加到布局文件中

<com.cccxm.ripple.RippleView
        android:id="@+id/mRippleView"
        android:layout_width="match_parent"
        android:layout_height="match_parent" />

目前為止我們只是創建了一個空的View,什么也沒有做,下一步,我們讓他能顯示圖片

1.2 顯示圖片

接下來,我們給View設置一個類型為Bitmap的屬性,并添加一個Set方法

var background: Bitmap? = null
        set(value) {
            field = value
            invalidate()
        }

當然,如果我們想顯示這個圖片的話,就必須重寫onDraw()方法

private val paint = Paint()

override fun onDraw(canvas: Canvas) {
    background?:return
    canvas.drawBitmap(background,0F,0F,paint)
}</code></pre> 

接下來我們在MainActivity的onCreate方法給View設置圖片

override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)
        mRippleView.background = BitmapFactory.decodeResource(resources, R.drawable.bac)
    }

1.2 運行效果

1.3 初識網格扭曲原理

現在,我們看一下網格扭曲需要的參數

public void drawBitmapMesh(Bitmap bitmap, int meshWidth, int meshHeight,
            float[] verts, int vertOffset, int[] colors, int colorOffset,
            Paint paint)

這里重點介紹 meshWidth 、 meshHeight 、 verts 三個參數。

  • meshWidth:網格的寬,這里指的是這個Bitmap橫向被分割成多少份
  • meshHeight:網格的高,這里指的是這個Bitmap被縱向分割成多少份
  • verts:這是一個數組,里面存放的是需要顯示的網格的坐標(后面詳細介紹)

1.3-1

上述圖片表示的是meshWidth=4;meshHeight=9,也就是說這張圖片被劃分成了36份,那么,verts里存放的又是什么?請看下圖

1.3-2

這里所有紅色圈圈住的(只標注了幾個,其他的沒有畫,主要是怕密恐狗投訴),從這里看出verts里存放的是每一個分割線焦點的坐標,包括屏幕邊緣。所以verts數組的大小為二倍的網格寬加一 網格高加一$$2 (meshWidth+1)*(meshHeight+1)$$為什么要乘2?因為坐標是以(x,y)形式成對存在的。

當我們調用扭曲方法時,其會從verts中依次取出各個坐標值,與原始坐標值比對,假如有原始坐標值為(10,10),但是verts中對應位置(比如數組中的第10,11位)的坐標值為(20,20),那么其就會通過一定的方法將(20,20)坐標附近的像素扭曲到(10,10)坐標附近。如下圖所示(畫圖略丑,輕吐槽)

1.3-3

1.3-4

1.4 實踐扭曲效果

通過1.3 節的講述,應該已經知道了扭曲的基本原理,接下來我們通過一個簡單的小實驗來看一下扭曲的效果

首先,聲明我們需要將圖片分割為橫30格,豎30格,和我們存儲坐標的數組

private val WIDTH = 30
    private val HEIGHT = 30
    private val COUNT = (WIDTH + 1) * (HEIGHT + 1)
    private val verts = FloatArray(COUNT * 2)
    private val orig = FloatArray(COUNT * 2)
解釋一下orig的作用,在這里我們聲明了一個和verts一樣的數組,里面存儲的是圖片原始的焦點與坐標對應的關系,如果沒有這個數組當我們修改verts造成扭曲效果之后就無法復原了。

接下來,我們在設置圖片的set方法中給數組賦值

var background: Bitmap? = null
        set(value) {
            field = value
            invalidate()
            val bitmapWidth = field!!.width.toFloat()
            val bitmapHeight = field!!.height.toFloat()
            var index = 0
            for (y in 0..HEIGHT) {
                val fy = bitmapHeight * y / HEIGHT
                for (x in 0..WIDTH) {
                    val fx = bitmapWidth * x / WIDTH
                    verts[index * 2 + 0] = fx
                    orig[index * 2 + 0] = fx
                    verts[index * 2 + 1] = fy
                    orig[index * 2 + 1] = fy
                    index++
                }
            }
        }

現在我們需要一個warp方法,其中的參數是手指點擊的坐標位置。該方法的作用是,將所有與手指點擊距離在200像素之內的方格進行扭曲偏移。

private fun warp(x: Float, y: Float) {
        for (i in 0..COUNT * 2 - 1 step 2) {
            val x1 = orig[i + 0]
            val y1 = orig[i + 1]
            val length = getLength(x1, y1, x, y)
            if (length < 200) {
                verts[i + 0] = orig[i + 0] + length * 0.5F//x軸偏移
                verts[i + 1] = orig[i + 1] + length * 0.5F//y軸偏移
            } else {
                verts[i + 0] = orig[i + 0]//x軸復原
                verts[i + 1] = orig[i + 1]//y軸復原
            }
        }
        invalidate()
    }

再然后,我們需要重寫View的觸摸方法

override fun onTouchEvent(event: MotionEvent): Boolean {
        when (event.action) {
            MotionEvent.ACTION_DOWN, MotionEvent.ACTION_MOVE -> {
                warp(event.x, event.y)
            }
            MotionEvent.ACTION_UP -> {
                verts.copyFrom(orig)
                invalidate()
            }
        }
        return true
    }

當手指按下和移動時計算扭曲,當手指離開的時候恢復圖片原貌。verts里面的copyFrom方法是我自己定義擴展的,代碼如下:

fun FloatArray.copyFrom(from: FloatArray) {
    var i = 0
    while (i < size && i < from.size)
        this[i] = from[i++]
}

現在萬事俱備,只差重繪,重寫僅僅將繪制圖片改為網格繪制即可

override fun onDraw(canvas: Canvas) {
        background ?: return
        canvas.drawBitmapMesh(background, WIDTH, HEIGHT, verts, 0, null, 0, null)
    }

1.4-1

二、繪制一圈深陷的波浪

現在開始正題了,波浪有波峰(Crests)和波谷(Troughs),我們預先設定一個原的半徑 initRadius=200 ,然后設置一個常量限制波的寬度 rippleWidth=20 (因為我們在計算偏移量時需要分半徑內和半徑外,所以實際這個參數表示波寬度的一半)

private val rippleWidth = 20F//波紋寬度
    private val initRadius = 200F//初始化半徑

接下來最重要的就是計算偏移量了

2.1 計算偏移量

首先我們根據測量兩點之間的距離判定一個點是否處于波的范圍內,如果在范圍內還要判斷是在半徑內和半徑外:

對于半徑外的計算圖如下所示(標題字寫錯了應該是半徑外)

2.1-1 位于半徑外的偏移量計算圖

可見對于半徑外的偏移點計算如下:(不造簡書為什么還不支持markdown的數學引擎)

$$\frac{length}{rate}=\frac{y1-y}{y2-y1}=\frac{x1-x}{x2-x1}$$

于是$$x2=x1+\frac{rate(x1-x)}{length}$$$$y2=y1+\frac{rate(y1-y)}{length}$$

然后我們創建方法

/**
     * 獲得波谷半徑外的偏移點
     * @param x0 原點x坐標
     * @param y0 原點y坐標
     * @param x1 需要偏移的點的x坐標
     * @param y1 需要偏移的點的y坐標
     */
    fun getTroughsOuter(x0: Float, y0: Float, x1: Float, y1: Float): PointF {
        val length = getLength(x0, y0, x1, y1)
        val rate = 20F
        val x = x1 + rate * (x1 - x0) / length
        val y = y1 + rate * (y1 - y0) / length
        return PointF(x, y)
    }

接下來計算半徑內的偏移點

2.1-2 位于半徑內的偏移量計算圖

可見對于半徑外的偏移點計算如下:

$$\frac{length}{rate}=\frac{y1-y0}{y1-y2}=\frac{x1-x0}{x1-x2}$$

于是$$x2=x1-\frac{rate(x1-x0)}{length}$$$$y2=y1-\frac{rate(y1-y0)}{length}$$

仍然創建方法

fun getTroughsInner(x0: Float, y0: Float, x1: Float, y1: Float): PointF {
        val length = getLength(x0, y0, x1, y1)
        val rate = 20F
        val x = x1 - rate * (x1 - x0) / length
        val y = y1 - rate * (y1 - y0) / length
        return PointF(x, y)
    }

2.2 初步測試

接下來編寫測試代碼,我們需要重構wrap方法,判斷點是否位于波內,在根據其位于半徑內和半徑外調用不同的方法

private fun warp(x0: Float, y0: Float) {
        for (i in 0..COUNT * 2 - 1 step 2) {
            val x1 = orig[i + 0]
            val y1 = orig[i + 1]
            val length = getLength(x0, y0, x1, y1)
            if (length < initRadius + rippleWidth && length > initRadius) {
                val point = getTroughsOuter(x0, y0, x1, y1)
                verts[i + 0] = point.x
                verts[i + 1] = point.y
            } else if (length < initRadius && length > initRadius - rippleWidth) {
                val point = getTroughsInner(x0, y0, x1, y1)
                verts[i + 0] = point.x
                verts[i + 1] = point.y
            } else {
                verts[i + 0] = orig[i + 0]//x軸復原
                verts[i + 1] = orig[i + 1]//y軸復原
            }
        }
        invalidate()
    }

2.2-1 演示圖片

2.3 波紋優化

通過上面的步驟發現已經有點意思了是吧! 但是這個波浪還是不夠動感,因為波浪的形狀類似正弦函數,但是我們上面方法中的rate值卻固定為了 val rate = 20F 。

2.3-1

為了得到更加真實的效果,我們編寫一個用于計算rate的函數,這個函數的結果與該點位置和中線位置的距離相關,并且符合正弦函數

/**
     * 計算波谷偏移量率
     */
    fun getTroughsRate(length: Float): Float {
        val dr = Math.abs(length - initRadius)
        val rate = dr * Math.PI / (2 * rippleWidth)
        return Math.sin(rate).toFloat() * rippleWidth
    }

接下來我們只需要將上述兩個計算方法中改為

val rate = getTroughsRate(length)

這里我將波紋寬度改為了10F

private val rippleWidth = 10F//波紋寬度

三、讓我們浪起來

我們上面的示例全部都是靜態的,當手指放在那里是才有波紋出現,而且波紋并不會擴散。

3.1 單個波紋

顧名思義,最開始我們需要讓一個波紋動起來已看效果,所以我們需要一個標志位標志現在是否有波浪正在顯示,并且我們需要一個線程可以不停地檢查刷新View,然后動態的改變半徑

1,動態修改半徑

原來我們有一個固定半徑的屬性initRadius,現在廢棄不用將所有需要用到半徑的方法增加一個參數

private fun warp(x0: Float, y0: Float, radius: Float);
private fun getTroughsInner(x0: Float, y0: Float, x1: Float, y1: Float, radius: Float): PointF;
private fun getTroughsOuter(x0: Float, y0: Float, x1: Float, y1: Float, radius: Float): PointF;
private fun getTroughsRate(length: Float, radius: Float): Float

2,創建全局變量存儲原點信息

private var originX = 0F
    private var originY = 0F
    private var isRipple = false
    override fun onTouchEvent(event: MotionEvent): Boolean {
        when (event.action) {
            MotionEvent.ACTION_DOWN -> {
                if (!isRipple) {
                    isRipple = true
                    originX = event.x
                    originY = event.y
                    loop.start()
                }
            }
            MotionEvent.ACTION_UP -> {
                verts.copyFrom(orig)
                invalidate()
            }
        }
        return true
    }

3,創建一個loop循環重繪(可以自定義線程實現),這個loop的作用是沒10毫秒循環一次,第count次設置半徑為radius,然后通知重繪,當radius>1000F時結束循環,結束時將標志位置位false

private val loop = ThreadUtils.Loop(10).loop { count ->
        val radius = count * 2F
        warp(originX, originY, radius)
        radius < 1000F
    }.onStop { isRipple = false }

OK!到這里就可以看到前面的效果了

 

 

 

來自:http://www.jianshu.com/p/11e6be1f18e6

 

 本文由用戶 minoru6560 自行上傳分享,僅供網友學習交流。所有權歸原作者,若您的權利被侵害,請聯系管理員。
 轉載本站原創文章,請注明出處,并保留原始鏈接、圖片水印。
 本站是一個以用戶分享為主的開源技術平臺,歡迎各類分享!