Android:使用drawBitmapMesh方法产生水波
<h2><strong>一、认识Canvas.drawBitmapMesh</strong></h2> <p>Mesh的含义是“网格”,也就是说它将整个Bitmap分成若干个网格,再对每一个网格进行相应的扭曲处理。至于其具体是怎么运作的,我们边做边说。</p> <h3><strong>1.1 创建一个View</strong></h3> <pre> <code class="language-java">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) }</code></pre> <p>当View创建完成后,我们将其添加到布局文件中</p> <pre> <code class="language-java"><com.cccxm.ripple.RippleView android:id="@+id/mRippleView" android:layout_width="match_parent" android:layout_height="match_parent" /></code></pre> <p>目前为止我们只是创建了一个空的View,什么也没有做,下一步,我们让他能显示图片</p> <h3><strong>1.2 显示图片</strong></h3> <p>接下来,我们给View设置一个类型为Bitmap的属性,并添加一个Set方法</p> <pre> <code class="language-java">var background: Bitmap? = null set(value) { field = value invalidate() }</code></pre> <p>当然,如果我们想显示这个图片的话,就必须重写onDraw()方法</p> <pre> <code class="language-java">private val paint = Paint() override fun onDraw(canvas: Canvas) { background?:return canvas.drawBitmap(background,0F,0F,paint) }</code></pre> <p>接下来我们在MainActivity的onCreate方法给View设置图片</p> <pre> <code class="language-java">override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContentView(R.layout.activity_main) mRippleView.background = BitmapFactory.decodeResource(resources, R.drawable.bac) }</code></pre> <p style="text-align:center"><img src="https://simg.open-open.com/show/1c2f9958eec8cc6fe507938a60bad313.png"></p> <p style="text-align:center">1.2 运行效果</p> <h3><strong>1.3 初识网格扭曲原理</strong></h3> <p>现在,我们看一下网格扭曲需要的参数</p> <pre> <code class="language-java">public void drawBitmapMesh(Bitmap bitmap, int meshWidth, int meshHeight, float[] verts, int vertOffset, int[] colors, int colorOffset, Paint paint)</code></pre> <p>这里重点介绍 meshWidth 、 meshHeight 、 verts 三个参数。</p> <ul> <li>meshWidth:网格的宽,这里指的是这个Bitmap横向被分割成多少份</li> <li>meshHeight:网格的高,这里指的是这个Bitmap被纵向分割成多少份</li> <li>verts:这是一个数组,里面存放的是需要显示的网格的坐标(后面详细介绍)</li> </ul> <p style="text-align:center"><img src="https://simg.open-open.com/show/8d3a7d6249bdaa43c7d8ae757ddd64e4.png"></p> <p style="text-align:center">1.3-1</p> <p>上述图片表示的是meshWidth=4;meshHeight=9,也就是说这张图片被划分成了36份,那么,verts里存放的又是什么?请看下图</p> <p style="text-align:center"><img src="https://simg.open-open.com/show/76612286889b5cb5cec3957bc7e97bc1.png"></p> <p style="text-align:center">1.3-2</p> <p>这里所有红色圈圈住的(只标注了几个,其他的没有画,主要是怕密恐狗投诉),从这里看出verts里存放的是每一个分割线焦点的坐标,包括屏幕边缘。所以verts数组的大小为二倍的网格宽加一 <em>网格高加一$$2</em> (meshWidth+1)*(meshHeight+1)$$为什么要乘2?因为坐标是以(x,y)形式成对存在的。</p> <p>当我们调用扭曲方法时,其会从verts中依次取出各个坐标值,与原始坐标值比对,假如有原始坐标值为(10,10),但是verts中对应位置(比如数组中的第10,11位)的坐标值为(20,20),那么其就会通过一定的方法将(20,20)坐标附近的像素扭曲到(10,10)坐标附近。如下图所示(画图略丑,轻吐槽)</p> <p style="text-align:center"><img src="https://simg.open-open.com/show/88072011022feed3c64e161ce6904630.png"></p> <p style="text-align:center">1.3-3</p> <p style="text-align:center"><img src="https://simg.open-open.com/show/1948012cb76743b9f711a1306036ebd5.png"></p> <p style="text-align:center">1.3-4</p> <h3><strong>1.4 实践扭曲效果</strong></h3> <p>通过1.3 节的讲述,应该已经知道了扭曲的基本原理,接下来我们通过一个简单的小实验来看一下扭曲的效果</p> <p>首先,声明我们需要将图片分割为横30格,竖30格,和我们存储坐标的数组</p> <pre> <code class="language-java">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)</code></pre> <pre> <code class="language-java">解释一下orig的作用,在这里我们声明了一个和verts一样的数组,里面存储的是图片原始的焦点与坐标对应的关系,如果没有这个数组当我们修改verts造成扭曲效果之后就无法复原了。</code></pre> <p>接下来,我们在设置图片的set方法中给数组赋值</p> <pre> <code class="language-java">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++ } } }</code></pre> <p>现在我们需要一个warp方法,其中的参数是手指点击的坐标位置。该方法的作用是,将所有与手指点击距离在200像素之内的方格进行扭曲偏移。</p> <pre> <code class="language-java">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() }</code></pre> <p>再然后,我们需要重写View的触摸方法</p> <pre> <code class="language-java">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 }</code></pre> <p>当手指按下和移动时计算扭曲,当手指离开的时候恢复图片原貌。verts里面的copyFrom方法是我自己定义扩展的,代码如下:</p> <pre> <code class="language-java">fun FloatArray.copyFrom(from: FloatArray) { var i = 0 while (i < size && i < from.size) this[i] = from[i++] }</code></pre> <p>现在万事俱备,只差重绘,重写仅仅将绘制图片改为网格绘制即可</p> <pre> <code class="language-java">override fun onDraw(canvas: Canvas) { background ?: return canvas.drawBitmapMesh(background, WIDTH, HEIGHT, verts, 0, null, 0, null) }</code></pre> <p style="text-align:center"><img src="https://simg.open-open.com/show/9d85c24006057ebf00a111eab55681c7.png"></p> <p style="text-align:center">1.4-1</p> <h2><strong>二、绘制一圈深陷的波浪</strong></h2> <p>现在开始正题了,波浪有波峰(Crests)和波谷(Troughs),我们预先设定一个原的半径 initRadius=200 ,然后设置一个常量限制波的宽度 rippleWidth=20 (因为我们在计算偏移量时需要分半径内和半径外,所以实际这个参数表示波宽度的一半)</p> <pre> <code class="language-java">private val rippleWidth = 20F//波纹宽度 private val initRadius = 200F//初始化半径</code></pre> <p>接下来最重要的就是计算偏移量了</p> <h3><strong>2.1 计算偏移量</strong></h3> <p>首先我们根据测量两点之间的距离判定一个点是否处于波的范围内,如果在范围内还要判断是在半径内和半径外:</p> <p>对于半径外的计算图如下所示(标题字写错了应该是半径外)</p> <p style="text-align:center"><img src="https://simg.open-open.com/show/f7f7d39309c0676ebe55406d177e7248.png"></p> <p style="text-align:center">2.1-1 位于半径外的偏移量计算图</p> <p>可见对于半径外的偏移点计算如下:(不造简书为什么还不支持markdown的数学引擎)</p> <p>$$\frac{length}{rate}=\frac{y1-y}{y2-y1}=\frac{x1-x}{x2-x1}$$</p> <p>于是$$x2=x1+\frac{rate(x1-x)}{length}$$$$y2=y1+\frac{rate(y1-y)}{length}$$</p> <p>然后我们创建方法</p> <pre> <code class="language-java">/** * 获得波谷半径外的偏移点 * @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) }</code></pre> <p>接下来计算半径内的偏移点</p> <p style="text-align:center"><img src="https://simg.open-open.com/show/4d9530e2549393d5cf039d599ac73938.png"></p> <p>2.1-2 位于半径内的偏移量计算图</p> <p>可见对于半径外的偏移点计算如下:</p> <p>$$\frac{length}{rate}=\frac{y1-y0}{y1-y2}=\frac{x1-x0}{x1-x2}$$</p> <p>于是$$x2=x1-\frac{rate(x1-x0)}{length}$$$$y2=y1-\frac{rate(y1-y0)}{length}$$</p> <p>仍然创建方法</p> <pre> <code class="language-java">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) }</code></pre> <h3><strong>2.2 初步测试</strong></h3> <p>接下来编写测试代码,我们需要重构wrap方法,判断点是否位于波内,在根据其位于半径内和半径外调用不同的方法</p> <pre> <code class="language-java">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() }</code></pre> <p style="text-align:center"><img src="https://simg.open-open.com/show/446f55c93f4b83735421c6935c42d97c.png"></p> <p style="text-align:center">2.2-1 演示图片</p> <h3><strong>2.3 波纹优化</strong></h3> <p>通过上面的步骤发现已经有点意思了是吧! 但是这个波浪还是不够动感,因为波浪的形状类似正弦函数,但是我们上面方法中的rate值却固定为了 val rate = 20F 。</p> <p style="text-align:center"><img src="https://simg.open-open.com/show/21db5b71fac538ae784ac54328f1b5f2.png"></p> <p style="text-align:center">2.3-1</p> <p>为了得到更加真实的效果,我们编写一个用于计算rate的函数,这个函数的结果与该点位置和中线位置的距离相关,并且符合正弦函数</p> <pre> <code class="language-java">/** * 计算波谷偏移量率 */ fun getTroughsRate(length: Float): Float { val dr = Math.abs(length - initRadius) val rate = dr * Math.PI / (2 * rippleWidth) return Math.sin(rate).toFloat() * rippleWidth }</code></pre> <p>接下来我们只需要将上述两个计算方法中改为</p> <pre> <code class="language-java">val rate = getTroughsRate(length)</code></pre> <p>这里我将波纹宽度改为了10F</p> <pre> <code class="language-java">private val rippleWidth = 10F//波纹宽度</code></pre> <h2><strong>三、让我们浪起来</strong></h2> <p>我们上面的示例全部都是静态的,当手指放在那里是才有波纹出现,而且波纹并不会扩散。</p> <h3><strong>3.1 单个波纹</strong></h3> <p>顾名思义,最开始我们需要让一个波纹动起来已看效果,所以我们需要一个标志位标志现在是否有波浪正在显示,并且我们需要一个线程可以不停地检查刷新View,然后动态的改变半径</p> <p>1,动态修改半径</p> <p>原来我们有一个固定半径的属性initRadius,现在废弃不用将所有需要用到半径的方法增加一个参数</p> <pre> <code class="language-java">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</code></pre> <p>2,创建全局变量存储原点信息</p> <pre> <code class="language-java">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 }</code></pre> <p>3,创建一个loop循环重绘(可以自定义线程实现),这个loop的作用是没10毫秒循环一次,第count次设置半径为radius,然后通知重绘,当radius>1000F时结束循环,结束时将标志位置位false</p> <pre> <code class="language-java">private val loop = ThreadUtils.Loop(10).loop { count -> val radius = count * 2F warp(originX, originY, radius) radius < 1000F }.onStop { isRipple = false }</code></pre> <p>OK!到这里就可以看到前面的效果了</p> <p> </p> <p> </p> <p> </p> <p>来自:http://www.jianshu.com/p/11e6be1f18e6</p> <p> </p>
本文由用户 minoru6560 自行上传分享,仅供网友学习交流。所有权归原作者,若您的权利被侵害,请联系管理员。
转载本站原创文章,请注明出处,并保留原始链接、图片水印。
本站是一个以用户分享为主的开源技术平台,欢迎各类分享!