1、ShapeDrawable
看到ShapeDrawable很自然就会想到shape标签,shape标签虽然可以实现和ShapeDrawable类似的效果,但是shape标签对应的是GradientDrawable而非ShapeDrawable。所以,我们在使用如下代码获取shape标签的实例,肯定会出现类型转换异常。
ShapeDrawable shapeDrawable=(ShapeDrawable)textview.getBackground();
神奇的是ShapeDrawable和GradientDrawable的用法基本一样。所以学会了ShapeDrawable的使用后,GradientDrawable的使用也不在话下了。
1.1、ShapeDrawable的构造函数
public ShapeDrawable()
public ShapeDrawable(Shape s)
ShapeDrawable需要和Shape对象关联在一起,在构造对象时传入Shape对象,若使用第一个函数构造ShapeDrawable,则还需要调用shapeDrawable.setShape(Shape s)与Shape进行关联。
在调用Drawable.draw(Canvas canvas)时会调用shape.draw()而Shape是一个抽象类,其中的draw()函数的实现,由其派生类实现。
2、Shape的派生类
Shap的派生类有如下几个:
- ArcShape:扇形shape
- OvalShape:椭圆形shape
- PathShape:构造一个可根据Path绘制的shape
- RectShape:矩形shape
- RoundRectShape:圆角矩形shape
2.1、RectShape
RectShape的实例
class RectShapeView(context: Context, attributeSet: AttributeSet) : View(context, attributeSet) {
private val rectDrawable = ShapeDrawable(RectShape())
override fun onDraw(canvas: Canvas) {
super.onDraw(canvas)
rectDrawable.setBounds(0, 0, 400, 400)
rectDrawable.paint.setColor(Color.YELLOW)
rectDrawable.draw(canvas)
}
}
在上面示例中,我们做了如下几件事:
- 1、构造ShapeDrawable()对象,并传入RectShape对象,将Drawable的形状指定为矩形。
- 2、通过ShapeDrawable.setBounds(0,0,400,400),指定了ShapeDrawable在当前控件中的位置。注意:这里的矩形位置是在控件中的位置,而不是在屏幕中的位置。
- 3、通过ShapeDrawable.getPaint()获取ShapDrawable中的默认画笔,并设置画笔颜色为黄色,这样ShapeDrawable就会被填充了黄色。
Drawable的画布问题
在上面示例中调用drawable.draw(canvas)的作用是将drawable画到RectShapeView控件上,那么黄色矩形是何时绘制的。
通过ShapeDrawable.getPaint()获取其自带的Paint,将画笔颜色设置为黄色,只要我们修改了画笔的颜色,它就会立刻在ShapeDrawable中重新绘制。然后调用ShapeDrawable.draw(canvas)将其绘制到ShpeView上。
2.2、OvalShape
OvalShape会根据ShapeDrawable.setBounds()设置的矩形生成一个椭圆
class RectShapeView(context: Context, attributeSet: AttributeSet) : View(context, attributeSet) {
private val rectDrawable = ShapeDrawable(OvalShape())
override fun onDraw(canvas: Canvas) {
super.onDraw(canvas)
rectDrawable.setBounds(0, 0, 400, 400)
rectDrawable.paint.setColor(Color.YELLOW)
rectDrawable.draw(canvas)
}
}
2.3、ArcShape
ArcShape是在OvalShape形成椭圆的基础上进行角度切割,X轴正方向为起始点,会根据设置的sweepAngle进行顺时针旋转。
public ArcShape(float startAngle, float sweepAngle)
- float startAngle:开始的角度,扇形开始的0°在X轴正方向上。
- float sweepAngle:扇形扫过的角度。
class RectShapeView(context: Context, attributeSet: AttributeSet) : View(context, attributeSet) {
private val rectDrawable = ShapeDrawable(ArcShape(0f,90f))
override fun onDraw(canvas: Canvas) {
super.onDraw(canvas)
rectDrawable.setBounds(0, 0, 400, 400)
rectDrawable.paint.setColor(Color.YELLOW)
rectDrawable.draw(canvas)
}
}
2.4、RoundRectShape
RoundRectShape字面意思是圆角矩形,其实它不仅能实现圆角矩形,它本意是镂空圆角矩形。
看下它的构造函数
public RoundRectShape(float[] outerRadii,RectF inset,float[] innerRadii)
- float[] outerRadii:外围矩形各个角的角度大小,需要填充8个元素,没两个数字一组,分别对应(左上角、右上角、右下角、左下角)4个角的角度。每两个数字构成一个椭圆,第一个数字表椭圆的X轴半径,第二个数字表示椭圆Y轴的半径。如果不需要指定外围矩形的角度,可以传入null。
- RectF inset:表示内部矩形和外部矩形的边距,RectF的4个值分别对应和四条边的边距。如果不需要内部矩形,则传入null。
- float[] innerRadii:表示内部矩形的各个角的角度,同样需要填充8个数字,和outerRadii意义一样,如果不需要指定内部矩形的各个角的大小,可传入null。
class RectShapeView(context: Context, attributeSet: AttributeSet) : View(context, attributeSet) {
private val outRadiusii= floatArrayOf(12f, 12f, 12f, 12f, 0f, 0f, 0f, 0f)
private val inset= RectF(50f,10f,50f,40f)
private val innerRadiusii= floatArrayOf(0f,0f,30f,30f,30f,30f,0f,0f)
private val rectDrawable = ShapeDrawable(RoundRectShape(outRadiusii,inset,innerRadiusii))
init {
setLayerType(LAYER_TYPE_SOFTWARE,null)
}
override fun onDraw(canvas: Canvas) {
super.onDraw(canvas)
rectDrawable.setBounds(0, 0, 400, 400)
rectDrawable.paint.setColor(Color.WHITE)
rectDrawable.draw(canvas)
}
}
2.5、PathShape
PathShape是一个可以根据路径绘制的Shape,构造函数如下
public PathShape(Path path, float stdWidth, float stdHeight)
- Path path:表示所有画的Path
- float stdWidth:表示标准宽度,即将整个ShapeDrawable的宽度分为多少份。Path.moveTo(x,y),lineTo(x2,y2)这些函数中的数值都是根据每一份的位置来计算的。当ShapeDrawable的动态变大、变小时,每一份都会变大变小的。
- float stdHeight:表示标准高度,将ShapeDrawable的高度分为多少份。
class RectShapeView(context: Context, attributeSet: AttributeSet) : View(context, attributeSet) {
private var rectDrawable: ShapeDrawable
init {
setLayerType(LAYER_TYPE_SOFTWARE, null)
val path = Path()
path.moveTo(0f, 0f)
path.lineTo(300f, 0f)
path.lineTo(300f, 300f)
path.lineTo(0f, 300f)
path.close()
rectDrawable= ShapeDrawable(PathShape(path,400f,400f))
}
override fun onDraw(canvas: Canvas) {
super.onDraw(canvas)
rectDrawable.setBounds(0, 0, 400, 400)
rectDrawable.paint.setColor(Color.WHITE)
rectDrawable.draw(canvas)
}
}
3、常用函数
3.1、setBounds
这个函数是用来指定,ShapeDrawable在当前控件中显示的位置
它的构造函数如下:
public void setBounds(int left, int top, int right, int bottom)
public void setBounds(@NonNull Rect bounds)
3.2、getPaint
getPaint()获取的是ShapeDrawable自带的Paint,只要操作Paint,效果就会立刻显示在ShapeDrawable中。
有关Paint需要注意一点:Paint.setShader(),Shader是从当前画布的左上角开始绘制,所以当ShapeDrawable的Paint调用Shader时,Shader是从ShapeDrawable的左上角开始绘制的。
下面通过一个例子,证明下Shader是从ShapeDrawable的左上角开始绘制的。
class RectShapeView(context: Context, attributeSet: AttributeSet) : View(context, attributeSet) {
private var rectDrawable: ShapeDrawable
private var bitmap = BitmapFactory.decodeResource(context.resources, R.mipmap.avator)
init {
setLayerType(LAYER_TYPE_SOFTWARE, null)
rectDrawable = ShapeDrawable(RectShape())
rectDrawable.setBounds(100, 100, 300,300)
val paint = rectDrawable.paint
paint.setShader(BitmapShader(bitmap, Shader.TileMode.CLAMP, Shader.TileMode.CLAMP))
}
override fun onDraw(canvas: Canvas) {
super.onDraw(canvas)
rectDrawable.draw(canvas)
}
}
效果图如下
我们通过setBounds()设置ShapeDrawable在控件中的位置为(100, 100, 300,300),然后可以看到图片是从(100, 100, 300,300)开始绘制的,而不是从RectShapeView控件的左上角,也不是从屏幕左上角开始的。
3.3、setIntrinsicHeight(int height)
函数声明如下
public void setIntrinsicHeight(int height)
setIntrinsicHeight()设置默认高度,当Drawable以setBackground()或setImageDrawable()方式使用时,会使用默认的宽高来计算当前Drawable的大小与位置。如果不设置,则默认的宽高为-1。
setIntrinsicWidth(int width)表示设置默认宽度。
3.4、放大镜效果
先看下效果图
这里会使用ShapeDrawable的Shader实现,将手指滑动到的位置放大3倍。
class TelescopeDrawableView(context: Context, attributeSet: AttributeSet) :
View(context, attributeSet) {
private var bitmap: Bitmap? = null
private var drawable: ShapeDrawable? = null
private val FACTOR = 3
private val mMatrix = Matrix()
private val RADIUS = 200
init {
setLayerType(LAYER_TYPE_SOFTWARE, null)
}
override fun onTouchEvent(event: MotionEvent): Boolean {
val x = event.x
val y = event.y
//表示Shader绘制开始的位置
mMatrix.setTranslate(RADIUS - FACTOR * x, RADIUS - FACTOR * y)
drawable?.paint?.shader?.setLocalMatrix(mMatrix)
drawable?.setBounds(
(x - RADIUS).toInt(),
(y - RADIUS).toInt(),
(x + RADIUS).toInt(),
(y + RADIUS).toInt()
)
invalidate()
return true
}
override fun onDraw(canvas: Canvas) {
super.onDraw(canvas)
if (bitmap == null) {
val srcBitmap = BitmapFactory.decodeResource(context.resources, R.mipmap.scenery)
bitmap = Bitmap.createScaledBitmap(srcBitmap, width, height, true)
val shader = BitmapShader(
Bitmap.createScaledBitmap(bitmap!!, width * FACTOR, height * FACTOR, true),
Shader.TileMode.CLAMP,
Shader.TileMode.CLAMP
)
drawable = ShapeDrawable(OvalShape())
drawable?.paint?.setShader(shader)
}
canvas.drawBitmap(bitmap!!, 0f, 0f, null)
drawable?.draw(canvas)
}
}
在onTouchEvent()方法中,手指移动通过setBounds()控制drawable在控件中的位置,由于Shader总是从ShapeDrawable的左上角开始绘制的,如果不移动Shader,那么永远显示的图片的左上角,如何移动Shader呢?
可以使用Shader.setLocalMatrix(Matrix localM)通过Matrix.setTranslate()来移动Shader。问题来了:如何移动到图片对应的点呢?
我们需要找到当前手指位置(x,y)在放大3倍后的图片上的位置,对应点就是(3x,3y),如果向左上移动分别移动3x、3y,那么移动后的点是在ShapeDrawable的左上角的,如果向让这个点在ShapeDrawable的中间点,就需要再向下、向右分别移动Radius,最终代码为
//表示Shader绘制开始的位置
mMatrix.setTranslate(RADIUS - FACTOR * x, RADIUS - FACTOR * y)
drawable?.paint?.shader?.setLocalMatrix(mMatrix)
4、自定义Drawable
下面通过实例完成自定义Drawable来实现圆角功能
class CustomDrawable:Drawable() {
override fun draw(canvas: Canvas) {
}
override fun setAlpha(alpha: Int) {
}
override fun setColorFilter(colorFilter: ColorFilter?) {
}
override fun getOpacity(): Int {
}
}
- draw(canvas: Canvas):类似于View的onDraw()函数,我们只需要调用Canvas.drawXXX()就可以在Drawable上绘制。
- setAlpha()和setColorFilter(),当外部调用CustomDrawable的这两个方法时,只需要将设置的参数设置给Paint即可。
- getOpacity():当外部需要知道我们自定义的CustomDrawable的显示模式时就会调用这个函数。取值有如下4个:
- PixelFormat.TRANSLUCENT: 表示当前 CustormDrawable 绘图是具
Alpha 通道的,即使用 CustornDrawable 后,其底部的图像仍有可能看得到。- PixelFormat.TRANSPARENT :表示当前 CustormDrawable 是完全透明的,其中什么都没画,如果使CustormDrawable ,则将完全显示其底部图像。
- PixelFormat.OPAQUE 表示当前的CustormDrawable 是完全没有 Ahpa 通道的,使用 CustormDrawable 后,其底层的图像将被完全覆盖,而只显示 CustormDrawable本身的图像。
- PixelFormat.UNKNOWN 表示未知。
一般而言,如果我们不知道该如何返回, 则直接返回PixelFormat. TRANSLUCENT 是最靠谱的做法。
4.1、实现圆角Drawable
我们先来看下完整的代码,下面自定义的CustomDrawable类所实现的功能是将传入的Bitmap转换成圆角的Bitmap。
class CustomDrawable(val bitmap: Bitmap) : Drawable() {
private val paint = Paint()
private var shader: BitmapShader? = null
private var bound: RectF = RectF()
init {
paint.isAntiAlias = true
}
override fun draw(canvas: Canvas) {
canvas.drawRoundRect(bound, 20f, 20f, paint)
}
override fun setBounds(left: Int, top: Int, right: Int, bottom: Int) {
super.setBounds(left, top, right, bottom)
shader = BitmapShader(
Bitmap.createScaledBitmap(bitmap, right - left, bottom - top, true),
Shader.TileMode.CLAMP,
Shader.TileMode.CLAMP
)
paint.setShader(shader)
bound.set(left.toFloat(), top.toFloat(), right.toFloat(), bottom.toFloat())
}
override fun setAlpha(alpha: Int) {
paint.alpha = alpha
}
override fun setColorFilter(colorFilter: ColorFilter?) {
paint.setColorFilter(colorFilter)
}
override fun getOpacity(): Int = PixelFormat.TRANSLUCENT
override fun getIntrinsicHeight(): Int {
return bitmap.height
}
override fun getIntrinsicWidth(): Int {
return bitmap.width
}
}
继承Drawable必须实现4个方法,有关setAlpha()和setColorFilter()很简单,只需要把传入的参数设置Paint即可。而关于getOpacity()直接返回PixelFormat.TRANSLUCENT即可。
在这里又多写了几个函数:
- getIntrinsicHeight()和getIntrinsicWidth():用于设置CustomDrawable的默认宽高,这里将Bitmap的宽高设置为默认宽高。
- setBounds(): 它的含义是给CustomDrawable设置位置和边界,即这块画布的大小。在setBounds函数中,我们创建了一个与Drawable大小相同的Bitmap作为CustomDrawable的Shader。也就是说Bitmap会根据Drawable的大小进行缩放,达到覆盖整个Drawable的效果。然后记录这个区域,方便在draw()函数中使用。
- draw():我们知道Shader是从画布的左上角开始绘制的,使用drawXXX()来控制显示的区域。
Drawable的使用方式
Drawable的使用方式有两种: - 1、使用ImageView.setImageDrawable(drawable),将Drawable设置为ImageView的源图像。
- 2、View.setBackground(drawable),将Drawable设置为View的背景
4.2、setImageDrawable(drawable)
我们在布局中定义一个ImageView控件
这里两点需要注意:
- 1、设置ImageView的背景为紫色。
- 2、设置scaleType="center"
然后再看下用法
val bitmap = BitmapFactory.decodeResource(resources, R.mipmap.guaguaka_text)
val customDrawable = CustomDrawable(bitmap)
iv.setImageDrawable(customDrawable)
效果如下
从效果图中可以看到,虽然我们将Bitmap缩放为整个边界大小,但是Drawable并没有覆盖整个ImageView,这又是为什么呢?
在这里我们使用setImageDrawable()设置数据,和在XML中给ImageView设置
android:src="@mipmap/avator"
一样都是给ImageView设置源图像,而源图像的大小和scaleType相关,我们这里设置的ScaleType为center
,所以ImageView必然会居中缩放图片,然后将图片的显示位置通过setBounds()函数设置给CustomDrawable。
也就是说setBounds()创建的画布大小和ScaleType相关,下面看下不同scaleType,显示的效果。
很明显,除了fitXY以外的模式下,ImageView会根据CustomDrawable的getIntrinsicHeight()、getIntrinsicWidth()中返回的宽高对Drawable进行等比拉伸,以适配ImageView。在计算出CustomDrawable的位置后,通过setBounds()函数传递给CustomDrawable显示。
4.3、setBackground(drawable)
下面使用setBackground(drawable)的方式来看下,此种方式如何计算出setBounds()的边界?
val bitmap = BitmapFactory.decodeResource(resources, R.mipmap.avator)
val customDrawable = CustomDrawable(bitmap)
tv.background=customDrawable
XML布局文件如下
效果图如下
从效果图中可以明显看出,宽度使用的是TextView的宽度,高度使用的是Drawable的高度。
之所以会出现这样的效果,是因为在使用setBackground()设置自定义Drawable时,控件的宽高计算会将将自定义Drawable的宽高和View的宽高进行比较,取最大值。控件的宽高确定后,然后通过setBounds()将控件所在的矩形区域设置给自定义Drawable。
正式由于setBackground()函数计算宽高特性,所以有时候我们不希望改变控件的wrap_content特性,也就是让控件的宽高以自己的宽高为准,而不考虑Drawable的宽高。解决这个问题,很简单,在在定义Drawable时不重写getIntrinsicHeight()、getIntrinsicWidth()即可,默认返回-1。效果如下:
总结:
- 1、当使用setImageDrawable(drawable)函数设置ImageView数据源时,会根据scaleType、ImageView控件的宽高、自定义Drawable的默认宽高,对Drawable进行缩放以适应ImageView,自定义Drawable的位置和大小和scaleType相关。
- 2、当使用setBackground()设置View的背景时,自定义Drawable的宽高和控件的宽高一致,当控件的宽高为wrap_content时,则会选取控件的宽高和自定义Drawable宽高的最大值,作为控件的宽高。