Android clipping
Clipping
在Android中如果多个view嵌套的会引起overdraw,很多时候一些view被覆盖了,对用户是不可见的,但是依然会进行绘制,这个时候使用clipping来进行对不可见区域进行裁剪,可以减少overdraw提高gpu的效率。 如下图所示:使用clipping rectangle就可以实现对view的裁剪。
使用clipping能达到的效果
clipping的使用也很简单: 1、裁剪出想要渲染的部分
canvas.clipRect(clipRectLeft,clipRectTop, clipRectRight,clipRectBottom)
如下只想渲染出300*300大小的区域
val rect2 = Rect(100, 100, 400, 400)
canvas.clipRect(rect2)
canvas.drawBitmap(bgBitmap, 0f, 0f, null)
2、裁剪出不需要渲染的部分
canvas.clipOutRect(clipRectLeft,clipRectTop, clipRectRight,clipRectBottom)
如下抠掉中间300*300的区域
val rect2 = Rect(100, 100, 400, 400)
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O) {
canvas.clipRect(rect2,Region.Op.DIFFERENCE)
} else {
canvas.clipOutRect(
rect2
)
}
canvas.drawBitmap(bgBitmap, 0f, 0f, null)
可以定义一个剪辑区域,并保存该状态。 然后平移画布,添加剪辑区域并旋转。 做一些绘图后,可以恢复原来的裁剪状态,可以继续做不同的平移和倾斜变换,如图所示。
clip方法 clip和clipOut方法的区别:通过clip的区域是显示的区域,通过clipOut方法是把该区域不显示。
实例
其实是两张图片叠加在一起形成的,后面是一个全屏的背景图,加上一个椭圆柱。
两张图片叠加的部分对用户不可见,属于过度绘制区,为了减少过度绘制对性能的影响可以用clipping方法对重叠的部分进行裁剪。 裁剪 可以先渲染出底部圆柱,然后再从背景中抠出底部区域的大小不用渲染,最后渲染背景:
上图中黑色区域代表clipOut区域 通过计算出底部圆柱的高度和宽度来对背景图进行一次裁剪这里自定义一个ClipImageDrawable继承了Drawable
class ClippedImageDrawable(context: Context) : Drawable() {
companion object {
private const val TAG = "ClippedImageDrawable"
}
private var bgBitmap: Bitmap =
BitmapFactory.decodeResource(context.resources, R.drawable.book_detail_bg)
private val shelfBitmap = BitmapFactory.decodeResource(context.resources, R.drawable.bookshelf)
override fun draw(canvas: Canvas) {
Log.d(TAG, "width ${bounds.width()} height ${bounds.height()}")
bounds.width()
val top = 0.71 * bounds.height()
val bottom = bounds.height()
val rect = Rect(0, top.toInt(), shelfBitmap.width, bottom)
canvas.drawBitmap(shelfBitmap, 0f, top.toFloat(), null)
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O) {
canvas.clipRect(rect, Region.Op.DIFFERENCE)
} else {
canvas.clipOutRect(rect)
}
canvas.drawBitmap(bgBitmap, 0f, 0f, null)
}
override fun setAlpha(alpha: Int) {
}
override fun setColorFilter(colorFilter: ColorFilter?) {
}
override fun getOpacity(): Int {
return PixelFormat.TRANSLUCENT
}
}
上面代码中先拿到背景和圆柱的bitmap对象,然后再背景图中裁剪出一个跟圆柱大小的rect,再执行
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O) {
canvas.clipRect(rect, Region.Op.DIFFERENCE)
} else {
canvas.clipOutRect(rect)
}
上面的canvas.clipRect(rect, Region.Op.DIFFERENCE)就是把rect和背景相交的部分裁剪,在Build.VERSION_CODES.O以上的版本可以使用canvas.clipOutRect(rect)方法。 注意上面代码的顺序: 1、先用canvas.drawBitmap底部的圆柱。 2、使用clip裁剪出圆柱图片的rect。 3、最后再绘制背景bitmp 在activity中用一下方式使用
val drawable = ClippedImageDrawable(this)
val imageView = ImageView(this).apply {
val param = FrameLayout.LayoutParams(
ViewGroup.LayoutParams.MATCH_PARENT,
ViewGroup.LayoutParams.MATCH_PARENT
)
scaleType = ImageView.ScaleType.FIT_XY
layoutParams = param
setImageDrawable(drawable)
}
setContentView(imageView)
给imageView设置drawable,效果如下:
看上面效果图中,圆柱和背景的相交处有两个空白,这是因为圆柱图片是一个长方形并且顶部有两处空白,这样绘制出来就达不到设计给的效果。这里的原因是在使用clip的时候用的是一个rect形状,要达到背景和圆柱融合的效果,使用rect形状是不行的。 可以使用clipoutPath方法来裁剪出想要的区域。 上面圆柱顶部的弧度可以用二次赛贝尔曲线来实现https://www.tweenmax.com.cn/tool/bezier/。
绘制二阶Bezier曲线 /** * 从上一个点开始,绘制二阶Bezier曲线 * (x1,y1)为控制点, (x2,y2)为终点 * 如果之前没有调用过 moveTo(),则默认从 (0,0)作为起点绘制。 / public void quadTo(float x1, float y1, float x2, float y2) ; /* * 和quadTo相同,只不过这里是使用的是相对坐标。 */ public void rQuadTo(float dx1, float dy1, float dx2, float dy2)
结合path和贝塞尔曲线绘制出底部的区域 path.moveTo(0f, top.toFloat()+40) path.quadTo( (bounds.width() / 2).toFloat(), top.toFloat()-50, bounds.width().toFloat(), (top + 60).toFloat() ) path.lineTo(bounds.width().toFloat(), bounds.height().toFloat()) path.lineTo(0f, bounds.height().toFloat())
如下图对底部区域执行clipOut之后
最终实现代码
package com.example.android.clippingexample
import android.content.Context
import android.graphics.*
import android.graphics.drawable.Drawable
import android.os.Build
import android.util.Log
class ClippedImageDrawable(context: Context) : Drawable() {
companion object {
private const val TAG = "ClippedImageDrawable"
}
private var bgBitmap: Bitmap =
BitmapFactory.decodeResource(context.resources, R.drawable.book_detail_bg)
private val shelfBitmap = BitmapFactory.decodeResource(context.resources, R.drawable.bookshelf)
private val path = Path()
override fun draw(canvas: Canvas) {
Log.d(TAG, "width ${bounds.width()} height ${bounds.height()}")
path.reset()
bounds.width()
val top = 0.71 * bounds.height()
path.moveTo(0f, top.toFloat()+40)
path.quadTo(
(bounds.width() / 2).toFloat(), top.toFloat()-50, bounds.width().toFloat(),
(top + 60).toFloat()
)
path.lineTo(bounds.width().toFloat(), bounds.height().toFloat())
path.lineTo(0f, bounds.height().toFloat())
path.close()
canvas.drawBitmap(shelfBitmap, 0f, top.toFloat(), null)
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O) {
canvas.clipPath(path, Region.Op.DIFFERENCE)
} else {
canvas.clipOutPath(path)
}
canvas.drawBitmap(bgBitmap, 0f, 0f, null)
}
override fun setAlpha(alpha: Int) {
}
override fun setColorFilter(colorFilter: ColorFilter?) {
}
override fun getOpacity(): Int {
return PixelFormat.TRANSLUCENT
}
}
效果
参考 1、https://medium.com/android-news/simplifying-layouts-with-layer-list-drawables-2f750ea1504e 2、https://blog.zen.ly/implementing-custom-drawables-part-1-5530a98cefc9 3、https://developer.android.google.cn/codelabs/advanced-android-kotlin-training-clipping-canvas-objects?hl=vi#0
|