[关闭]
@cxm-2016 2016-09-21T17:01:53.000000Z 字数 6603 阅读 5141

Android:使用drawBitmapMesh方法产生水波(一)

kotlin android no
作者:陈小默


前言:这篇博客并没有什么好看的,众爱卿都退下吧


一、认识Canvas.drawBitmapMesh

Mesh的含义是“网格”,也就是说它将整个Bitmap分成若干个网格,再对每一个网格进行相应的扭曲处理。至于其具体是怎么运作的,我们边做边说。by:陈小默

1.1 创建一个View

  1. class RippleView : View {
  2. constructor(context: Context?) : super(context)
  3. constructor(context: Context?, attrs: AttributeSet?) : super(context, attrs)
  4. constructor(context: Context?, attrs: AttributeSet?, defStyleAttr: Int) : super(context, attrs, defStyleAttr)
  5. }

当View创建完成后,我们将其添加到布局文件中

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

目前为止我们只是创建了一个空的View,什么也没有做,下一步,我们让他能显示图片

1.2 显示图片

接下来,我们给View设置一个类型为Bitmap的属性,并添加一个Set方法

  1. var background: Bitmap? = null
  2. set(value) {
  3. field = value
  4. invalidate()
  5. }

当然,如果我们想显示这个图片的话,就必须重写onDraw()方法

  1. private val paint = Paint()
  2. override fun onDraw(canvas: Canvas) {
  3. background?:return
  4. canvas.drawBitmap(background,0F,0F,paint)
  5. }

接下来我们在MainActivity的onCreate方法给View设置图片

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

1.2 运行效果

1.3 初识网格扭曲原理

现在,我们看一下网格扭曲需要的参数

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

这里重点介绍meshWidthmeshHeightverts三个参数。

1.3-1
上述图片表示的是meshWidth=4;meshHeight=9,也就是说这张图片被划分成了36份,那么,verts里存放的又是什么?请看下图
1.3-2
这里所有红色圈圈住的(只标注了几个,其他的没有画,主要是怕密恐狗投诉),从这里看出verts里存放的是每一个分割线焦点的坐标,包括屏幕边缘。所以verts数组的大小为二倍的网格宽加一*网格高加一

为什么要乘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格,和我们存储坐标的数组

  1. private val WIDTH = 30
  2. private val HEIGHT = 30
  3. private val COUNT = (WIDTH + 1) * (HEIGHT + 1)
  4. private val verts = FloatArray(COUNT * 2)
  5. private val orig = FloatArray(COUNT * 2)
解释一下orig的作用,在这里我们声明了一个和verts一样的数组,里面存储的是图片原始的焦点与坐标对应的关系,如果没有这个数组当我们修改verts造成扭曲效果之后就无法复原了。

接下来,我们在设置图片的set方法中给数组赋值

  1. var background: Bitmap? = null
  2. set(value) {
  3. field = value
  4. invalidate()
  5. val bitmapWidth = field!!.width.toFloat()
  6. val bitmapHeight = field!!.height.toFloat()
  7. var index = 0
  8. for (y in 0..HEIGHT) {
  9. val fy = bitmapHeight * y / HEIGHT
  10. for (x in 0..WIDTH) {
  11. val fx = bitmapWidth * x / WIDTH
  12. verts[index * 2 + 0] = fx
  13. orig[index * 2 + 0] = fx
  14. verts[index * 2 + 1] = fy
  15. orig[index * 2 + 1] = fy
  16. index++
  17. }
  18. }
  19. }

现在我们需要一个warp方法,其中的参数是手指点击的坐标位置。该方法的作用是,将所有与手指点击距离在200像素之内的方格进行扭曲偏移。

  1. private fun warp(x: Float, y: Float) {
  2. for (i in 0..COUNT * 2 - 1 step 2) {
  3. val x1 = orig[i + 0]
  4. val y1 = orig[i + 1]
  5. val length = getLength(x1, y1, x, y)
  6. if (length < 200) {
  7. verts[i + 0] = orig[i + 0] + length * 0.5F//x轴偏移
  8. verts[i + 1] = orig[i + 1] + length * 0.5F//y轴偏移
  9. } else {
  10. verts[i + 0] = orig[i + 0]//x轴复原
  11. verts[i + 1] = orig[i + 1]//y轴复原
  12. }
  13. }
  14. invalidate()
  15. }

再然后,我们需要重写View的触摸方法

  1. override fun onTouchEvent(event: MotionEvent): Boolean {
  2. when (event.action) {
  3. MotionEvent.ACTION_DOWN, MotionEvent.ACTION_MOVE -> {
  4. warp(event.x, event.y)
  5. }
  6. MotionEvent.ACTION_UP -> {
  7. verts.copyFrom(orig)
  8. invalidate()
  9. }
  10. }
  11. return true
  12. }

当手指按下和移动时计算扭曲,当手指离开的时候恢复图片原貌。verts里面的copyFrom方法是我自己定义扩展的,代码如下:

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

现在万事俱备,只差重绘,重写仅仅将绘制图片改为网格绘制即可

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

1.4-1


二、绘制一圈深陷的波浪

现在开始正题了,波浪有波峰(Crests)和波谷(Troughs),我们预先设定一个原的半径initRadius=200,然后设置一个常量限制波的宽度rippleWidth=20(因为我们在计算偏移量时需要分半径内和半径外,所以实际这个参数表示波宽度的一半)

  1. private val rippleWidth = 20F//波纹宽度
  2. private val initRadius = 200F//初始化半径

接下来最重要的就是计算偏移量了

2.1 计算偏移量

首先我们根据测量两点之间的距离判定一个点是否处于波的范围内,如果在范围内还要判断是在半径内和半径外:
对于半径外的计算图如下所示(标题字写错了应该是半径外)
2.1-1 位于半径外的偏移量计算图
可见对于半径外的偏移点计算如下:


于是

然后我们创建方法

  1. /**
  2. * 获得波谷半径外的偏移点
  3. * @param x0 原点x坐标
  4. * @param y0 原点y坐标
  5. * @param x1 需要偏移的点的x坐标
  6. * @param y1 需要偏移的点的y坐标
  7. */
  8. fun getTroughsOuter(x0: Float, y0: Float, x1: Float, y1: Float): PointF {
  9. val length = getLength(x0, y0, x1, y1)
  10. val rate = 20F
  11. val x = x1 + rate * (x1 - x0) / length
  12. val y = y1 + rate * (y1 - y0) / length
  13. return PointF(x, y)
  14. }

接下来计算半径内的偏移点
2.1-2 位于半径内的偏移量计算图
可见对于半径外的偏移点计算如下:


于是

仍然创建方法

  1. fun getTroughsInner(x0: Float, y0: Float, x1: Float, y1: Float): PointF {
  2. val length = getLength(x0, y0, x1, y1)
  3. val rate = 20F
  4. val x = x1 - rate * (x1 - x0) / length
  5. val y = y1 - rate * (y1 - y0) / length
  6. return PointF(x, y)
  7. }

2.2 初步测试

接下来编写测试代码,我们需要重构wrap方法,判断点是否位于波内,在根据其位于半径内和半径外调用不同的方法

  1. private fun warp(x0: Float, y0: Float) {
  2. for (i in 0..COUNT * 2 - 1 step 2) {
  3. val x1 = orig[i + 0]
  4. val y1 = orig[i + 1]
  5. val length = getLength(x0, y0, x1, y1)
  6. if (length < initRadius + rippleWidth && length > initRadius) {
  7. val point = getTroughsOuter(x0, y0, x1, y1)
  8. verts[i + 0] = point.x
  9. verts[i + 1] = point.y
  10. } else if (length < initRadius && length > initRadius - rippleWidth) {
  11. val point = getTroughsInner(x0, y0, x1, y1)
  12. verts[i + 0] = point.x
  13. verts[i + 1] = point.y
  14. } else {
  15. verts[i + 0] = orig[i + 0]//x轴复原
  16. verts[i + 1] = orig[i + 1]//y轴复原
  17. }
  18. }
  19. invalidate()
  20. }

2.2-1 演示图片

2.3 波纹优化

通过上面的步骤发现已经有点意思了是吧! 但是这个波浪还是不够动感,因为波浪的形状类似正弦函数,但是我们上面方法中的rate值却固定为了val rate = 20F

2.3-1
为了得到更加真实的效果,我们编写一个用于计算rate的函数,这个函数的结果与该点位置和中线位置的距离相关,并且符合正弦函数

  1. /**
  2. * 计算波谷偏移量率
  3. */
  4. fun getTroughsRate(length: Float): Float {
  5. val dr = Math.abs(length - initRadius)
  6. val rate = dr * Math.PI / (2 * rippleWidth)
  7. return Math.sin(rate).toFloat() * rippleWidth
  8. }

接下来我们只需要将上述两个计算方法中改为

  1. val rate = getTroughsRate(length)

这里我将波纹宽度改为了10F

  1. private val rippleWidth = 10F//波纹宽度

三、让我们浪起来

我们上面的示例全部都是静态的,当手指放在那里是才有波纹出现,而且波纹并不会扩散。

3.1 单个波纹

顾名思义,最开始我们需要让一个波纹动起来已看效果,所以我们需要一个标志位标志现在是否有波浪正在显示,并且我们需要一个线程可以不停地检查刷新View,然后动态的改变半径

1,动态修改半径
原来我们有一个固定半径的属性initRadius,现在废弃不用将所有需要用到半径的方法增加一个参数

  1. private fun warp(x0: Float, y0: Float, radius: Float);
  2. private fun getTroughsInner(x0: Float, y0: Float, x1: Float, y1: Float, radius: Float): PointF;
  3. private fun getTroughsOuter(x0: Float, y0: Float, x1: Float, y1: Float, radius: Float): PointF;
  4. private fun getTroughsRate(length: Float, radius: Float): Float

2,创建全局变量存储原点信息

  1. private var originX = 0F
  2. private var originY = 0F
  3. private var isRipple = false
  4. override fun onTouchEvent(event: MotionEvent): Boolean {
  5. when (event.action) {
  6. MotionEvent.ACTION_DOWN -> {
  7. if (!isRipple) {
  8. isRipple = true
  9. originX = event.x
  10. originY = event.y
  11. loop.start()
  12. }
  13. }
  14. MotionEvent.ACTION_UP -> {
  15. verts.copyFrom(orig)
  16. invalidate()
  17. }
  18. }
  19. return true
  20. }

3,创建一个loop循环重绘(可以自定义线程实现),这个loop的作用是没10毫秒循环一次,第count次设置半径为radius,然后通知重绘,当radius>1000F时结束循环,结束时将标志位置位false

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

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

到目前为止我们仅仅是做出了一层未经任何处理的波浪,如果我们想让效果看起来更加逼真,请看 Android:使用drawBitmapMesh方法产生水波(二)

其实我并不打算写第二篇:) 这一篇就是用来学习drawBitmapMesh的使用方法的,来咬我呀,哈哈哈。。。
添加新批注
在作者公开此批注前,只有你和作者可见。
回复批注