Android 实现水波纹
fmms
13年前
在Android中,每一个图像像素通过一个4字节整数来展现:最高位字节用作alpha通道,接下来的事Red,依次类推,接下来的两个字节对应实现Green和Bule。 <br /> <br /> 要达到现实的水波效果比较难,这里一切从简了。 <br /> <br /> 先复习一下物理学。在一滩平静的水面(所有点的振幅为0),扔上一个半径为r的圆形石头,则第一时间水面上被石头打到的那部分水就会往下沉(振幅变为负)。然后,每一个被打到的点都会把这刚刚获取的能量往四周扩散(在这个例子中,假设只有上下左右四个方向的点受到中心的影响,说了一切从简的),同时,由于扩散的过程当中的能量损失,振幅会变得越来越小,直至整个水面恢复平静。 <br /> <br /> 折射,在一张背景图片模拟水波效果的重点在于模拟水波的折射效果。出现水波的时候,相邻两个点之间的高度不一致,出现了一定的高度差,假定我们从正上方看这个水波,这个高度差就会产生一个折射效果,即我们看到的点应该在实际位置的偏下位置。一切从简的话,这个位置偏移多少就直接由这个高度差来决定算了。就简单模拟一下,其实运行之后的效果也不是那么的差。 <br /> <br /> <strong> 一段段代码的分析:</strong> <br /> <br /> buf1和buf2分别用来存储一个像素点在一次渲染前和渲染后的振幅,BitMap1,BitMap2用来存储获得的图片像素 <br /> <pre class="brush:java; toolbar: true; auto-links: false;">short[] buf2; short[] buf1; int[] Bitmap2; int[] Bitmap1; buf2 = new short[BACKWIDTH * BACKHEIGHT]; buf1 = new short[BACKWIDTH * BACKHEIGHT]; Bitmap2 = new int[BACKWIDTH * BACKHEIGHT]; Bitmap1 = new int[BACKWIDTH * BACKHEIGHT];</pre> <p></p> 扔石头,如上所述 <br /> 则第一时间水面上被石头打到的那部分水就会往下沉(振幅变为负)。 <br /> <pre class="brush:java; toolbar: true; auto-links: false;">void DropStone(int x,// x坐标 int y,// y坐标 int stonesize,// 波源半径 int stoneweight)// 波源能量 { for (int posx = x - stonesize; posx < x + stonesize; posx++) for (int posy = y - stonesize; posy < y + stonesize; posy++) if ((posx - x) * (posx - x) + (posy - y) * (posy - y) < stonesize* stonesize) buf1[BACKWIDTH * posy + posx] = (short) -stoneweight; }</pre> <p></p> 这一段则是现实扩散的过程。buf2为扩散之后的振幅,由于只考虑上下左右四个方向对中心振幅的影响,那么影响一个点在一次扩散之后的振幅为四周的振幅和自己上一次的振幅。四周的影响假定相同。设X为中心处得振幅,上下左右振幅为X1,X2,X3,X4,X’为一次扩散之后的振幅则。X’=(X1 +X2+X3+X4)*a+X*b。 这个扩散是相对的,四个方向同样要受到中心点的影响,非常粗虐的计算,根据能量守恒,同时把这个局部当做整体。X+ X1 + X2+ X3 +X4=X'+X1' +X2'+ X3' + X4'。代入之后得出结果 4a+b=1。取一组合理的解为a=1/2,b=-1。为了提高效率,将除以2变成移位。得到新的振幅之后,执行衰减。 <br /> <pre class="brush:java; toolbar: true; auto-links: false;">void RippleSpread() { for (int i = BACKWIDTH; i < BACKWIDTH * BACKHEIGHT - BACKWIDTH; i++) { // 波能扩散 buf2[i] = (short) (((buf1[i - 1] + buf1[i + 1]+ buf1[i - BACKWIDTH] + buf1[i + BACKWIDTH]) >> 1) - buf2[i]); // 波能衰减 buf2[i] -= buf2[i] >> 5; } // 交换波能数据缓冲区 short[] ptmp = buf1; buf1 = buf2; buf2 = ptmp; }</pre> <p></p> <p> 扩散一次之后,则根据相邻像素点之间的高度差(即振幅差)计算出偏移量Xoff和Yoff,上面说过的,偏移就直接等于高度差算了,一切从简。然后将偏移加到具体的像素里头,新的像素为原来的像素加上偏移之后的像素。</p> <p></p> <pre class="brush:java; toolbar: true; auto-links: false;">/* 渲染你水纹效果 */ void render() { int xoff, yoff; int k = BACKWIDTH; for (int i = 1; i < BACKHEIGHT - 1; i++) { for (int j = 0; j < BACKWIDTH; j++) { // 计算偏移量 xoff = buf1[k - 1] - buf1[k + 1]; yoff = buf1[k - BACKWIDTH] - buf1[k + BACKWIDTH]; // 判断坐标是否在窗口范围内 if ((i + yoff) < 0) { k++; continue; } if ((i + yoff) > BACKHEIGHT) { k++; continue; } if ((j + xoff) < 0) { k++; continue; } if ((j + xoff) > BACKWIDTH) { k++; continue; } // 计算出偏移象素和原始象素的内存地址偏移量 int pos1, pos2; pos1 = BACKWIDTH * (i + yoff) + (j + xoff); pos2 = BACKWIDTH * i + j; Bitmap2[pos2++] = Bitmap1[pos1++]; k++; } } }</pre> <p></p> 最后,把这些加到线程里头,DropStone方法在onKeyUp事件中调用,线程绘图了,每过50ms就扩撒一次,直至水面平静。 <p></p> <p></p> <pre class="brush:java; toolbar: true; auto-links: false;">public void run() { while (!Thread.currentThread().isInterrupted()) { try { Thread.sleep(50); } catch (InterruptedException e) { Thread.currentThread().interrupt(); } RippleSpread(); render(); // 使用postInvalidate可以直接在线程中更新界面 postInvalidate(); } }</pre> <p></p>