1、ShapeDrawable
看到ShapeDrawable很自然就會想到shape標(biāo)簽,shape標(biāo)簽雖然可以實(shí)現(xiàn)和ShapeDrawable類似的效果,但是shape標(biāo)簽對應(yīng)的是GradientDrawable而非ShapeDrawable。所以,我們在使用如下代碼獲取shape標(biāo)簽的實(shí)例,肯定會出現(xiàn)類型轉(zhuǎn)換異常。
ShapeDrawable shapeDrawable=(ShapeDrawable)textview.getBackground();
神奇的是ShapeDrawable和GradientDrawable的用法基本一樣。所以學(xué)會了ShapeDrawable的使用后,GradientDrawable的使用也不在話下了。
1.1、ShapeDrawable的構(gòu)造函數(shù)
public ShapeDrawable()
public ShapeDrawable(Shape s)
ShapeDrawable需要和Shape對象關(guān)聯(lián)在一起,在構(gòu)造對象時(shí)傳入Shape對象,若使用第一個(gè)函數(shù)構(gòu)造ShapeDrawable,則還需要調(diào)用shapeDrawable.setShape(Shape s)與Shape進(jìn)行關(guān)聯(lián)。
在調(diào)用Drawable.draw(Canvas canvas)時(shí)會調(diào)用shape.draw()而Shape是一個(gè)抽象類,其中的draw()函數(shù)的實(shí)現(xiàn),由其派生類實(shí)現(xiàn)。
2、Shape的派生類
Shap的派生類有如下幾個(gè):
- ArcShape:扇形shape
- OvalShape:橢圓形shape
- PathShape:構(gòu)造一個(gè)可根據(jù)Path繪制的shape
- RectShape:矩形shape
- RoundRectShape:圓角矩形shape
2.1、RectShape
RectShape的實(shí)例
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、構(gòu)造ShapeDrawable()對象,并傳入RectShape對象,將Drawable的形狀指定為矩形。
- 2、通過ShapeDrawable.setBounds(0,0,400,400),指定了ShapeDrawable在當(dāng)前控件中的位置。注意:這里的矩形位置是在控件中的位置,而不是在屏幕中的位置。
- 3、通過ShapeDrawable.getPaint()獲取ShapDrawable中的默認(rèn)畫筆,并設(shè)置畫筆顏色為黃色,這樣ShapeDrawable就會被填充了黃色。
Drawable的畫布問題
在上面示例中調(diào)用drawable.draw(canvas)的作用是將drawable畫到RectShapeView控件上,那么黃色矩形是何時(shí)繪制的。
通過ShapeDrawable.getPaint()獲取其自帶的Paint,將畫筆顏色設(shè)置為黃色,只要我們修改了畫筆的顏色,它就會立刻在ShapeDrawable中重新繪制。然后調(diào)用ShapeDrawable.draw(canvas)將其繪制到ShpeView上。
2.2、OvalShape
OvalShape會根據(jù)ShapeDrawable.setBounds()設(shè)置的矩形生成一個(gè)橢圓
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形成橢圓的基礎(chǔ)上進(jìn)行角度切割,X軸正方向?yàn)槠鹗键c(diǎn),會根據(jù)設(shè)置的sweepAngle進(jìn)行順時(shí)針旋轉(zhuǎn)。
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字面意思是圓角矩形,其實(shí)它不僅能實(shí)現(xiàn)圓角矩形,它本意是鏤空圓角矩形。
看下它的構(gòu)造函數(shù)
public RoundRectShape(float[] outerRadii,RectF inset,float[] innerRadii)
- float[] outerRadii:外圍矩形各個(gè)角的角度大小,需要填充8個(gè)元素,沒兩個(gè)數(shù)字一組,分別對應(yīng)(左上角、右上角、右下角、左下角)4個(gè)角的角度。每兩個(gè)數(shù)字構(gòu)成一個(gè)橢圓,第一個(gè)數(shù)字表橢圓的X軸半徑,第二個(gè)數(shù)字表示橢圓Y軸的半徑。如果不需要指定外圍矩形的角度,可以傳入null。
- RectF inset:表示內(nèi)部矩形和外部矩形的邊距,RectF的4個(gè)值分別對應(yīng)和四條邊的邊距。如果不需要內(nèi)部矩形,則傳入null。
- float[] innerRadii:表示內(nèi)部矩形的各個(gè)角的角度,同樣需要填充8個(gè)數(shù)字,和outerRadii意義一樣,如果不需要指定內(nèi)部矩形的各個(gè)角的大小,可傳入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是一個(gè)可以根據(jù)路徑繪制的Shape,構(gòu)造函數(shù)如下
public PathShape(Path path, float stdWidth, float stdHeight)
- Path path:表示所有畫的Path
- float stdWidth:表示標(biāo)準(zhǔn)寬度,即將整個(gè)ShapeDrawable的寬度分為多少份。Path.moveTo(x,y),lineTo(x2,y2)這些函數(shù)中的數(shù)值都是根據(jù)每一份的位置來計(jì)算的。當(dāng)ShapeDrawable的動態(tài)變大、變小時(shí),每一份都會變大變小的。
- float stdHeight:表示標(biāo)準(zhǔn)高度,將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、常用函數(shù)
3.1、setBounds
這個(gè)函數(shù)是用來指定,ShapeDrawable在當(dāng)前控件中顯示的位置
它的構(gòu)造函數(shù)如下:
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中。
有關(guān)Paint需要注意一點(diǎn):Paint.setShader(),Shader是從當(dāng)前畫布的左上角開始繪制,所以當(dāng)ShapeDrawable的Paint調(diào)用Shader時(shí),Shader是從ShapeDrawable的左上角開始繪制的。
下面通過一個(gè)例子,證明下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()設(shè)置ShapeDrawable在控件中的位置為(100, 100, 300,300),然后可以看到圖片是從(100, 100, 300,300)開始繪制的,而不是從RectShapeView控件的左上角,也不是從屏幕左上角開始的。
3.3、setIntrinsicHeight(int height)
函數(shù)聲明如下
public void setIntrinsicHeight(int height)
setIntrinsicHeight()設(shè)置默認(rèn)高度,當(dāng)Drawable以setBackground()或setImageDrawable()方式使用時(shí),會使用默認(rèn)的寬高來計(jì)算當(dāng)前Drawable的大小與位置。如果不設(shè)置,則默認(rèn)的寬高為-1。
setIntrinsicWidth(int width)表示設(shè)置默認(rèn)寬度。
3.4、放大鏡效果
先看下效果圖

這里會使用ShapeDrawable的Shader實(shí)現(xiàn),將手指滑動到的位置放大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,那么永遠(yuǎn)顯示的圖片的左上角,如何移動Shader呢?
可以使用Shader.setLocalMatrix(Matrix localM)通過Matrix.setTranslate()來移動Shader。問題來了:如何移動到圖片對應(yīng)的點(diǎn)呢?
我們需要找到當(dāng)前手指位置(x,y)在放大3倍后的圖片上的位置,對應(yīng)點(diǎn)就是(3x,3y),如果向左上移動分別移動3x、3y,那么移動后的點(diǎn)是在ShapeDrawable的左上角的,如果向讓這個(gè)點(diǎn)在ShapeDrawable的中間點(diǎn),就需要再向下、向右分別移動Radius,最終代碼為
//表示Shader繪制開始的位置
mMatrix.setTranslate(RADIUS - FACTOR * x, RADIUS - FACTOR * y)
drawable?.paint?.shader?.setLocalMatrix(mMatrix)
4、自定義Drawable
下面通過實(shí)例完成自定義Drawable來實(shí)現(xiàn)圓角功能
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()函數(shù),我們只需要調(diào)用Canvas.drawXXX()就可以在Drawable上繪制。
- setAlpha()和setColorFilter(),當(dāng)外部調(diào)用CustomDrawable的這兩個(gè)方法時(shí),只需要將設(shè)置的參數(shù)設(shè)置給Paint即可。
- getOpacity():當(dāng)外部需要知道我們自定義的CustomDrawable的顯示模式時(shí)就會調(diào)用這個(gè)函數(shù)。取值有如下4個(gè):
- PixelFormat.TRANSLUCENT: 表示當(dāng)前 CustormDrawable 繪圖是具
Alpha 通道的,即使用 CustornDrawable 后,其底部的圖像仍有可能看得到。- PixelFormat.TRANSPARENT :表示當(dāng)前 CustormDrawable 是完全透明的,其中什么都沒畫,如果使CustormDrawable ,則將完全顯示其底部圖像。
- PixelFormat.OPAQUE 表示當(dāng)前的CustormDrawable 是完全沒有 Ahpa 通道的,使用 CustormDrawable 后,其底層的圖像將被完全覆蓋,而只顯示 CustormDrawable本身的圖像。
- PixelFormat.UNKNOWN 表示未知。
一般而言,如果我們不知道該如何返回, 則直接返回PixelFormat. TRANSLUCENT 是最靠譜的做法。
4.1、實(shí)現(xiàn)圓角Drawable
我們先來看下完整的代碼,下面自定義的CustomDrawable類所實(shí)現(xiàn)的功能是將傳入的Bitmap轉(zhuǎn)換成圓角的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必須實(shí)現(xiàn)4個(gè)方法,有關(guān)setAlpha()和setColorFilter()很簡單,只需要把傳入的參數(shù)設(shè)置Paint即可。而關(guān)于getOpacity()直接返回PixelFormat.TRANSLUCENT即可。
在這里又多寫了幾個(gè)函數(shù):
- getIntrinsicHeight()和getIntrinsicWidth():用于設(shè)置CustomDrawable的默認(rèn)寬高,這里將Bitmap的寬高設(shè)置為默認(rèn)寬高。
- setBounds(): 它的含義是給CustomDrawable設(shè)置位置和邊界,即這塊畫布的大小。在setBounds函數(shù)中,我們創(chuàng)建了一個(gè)與Drawable大小相同的Bitmap作為CustomDrawable的Shader。也就是說Bitmap會根據(jù)Drawable的大小進(jìn)行縮放,達(dá)到覆蓋整個(gè)Drawable的效果。然后記錄這個(gè)區(qū)域,方便在draw()函數(shù)中使用。
- draw():我們知道Shader是從畫布的左上角開始繪制的,使用drawXXX()來控制顯示的區(qū)域。
Drawable的使用方式
Drawable的使用方式有兩種: - 1、使用ImageView.setImageDrawable(drawable),將Drawable設(shè)置為ImageView的源圖像。
- 2、View.setBackground(drawable),將Drawable設(shè)置為View的背景
4.2、setImageDrawable(drawable)
我們在布局中定義一個(gè)ImageView控件
<ImageView
android:id="@+id/iv"
android:layout_width="200dp"
android:layout_height="100dp"
android:background="@color/purple_200"
android:scaleType="center" />
這里兩點(diǎn)需要注意:
- 1、設(shè)置ImageView的背景為紫色。
- 2、設(shè)置scaleType="center"
然后再看下用法
val bitmap = BitmapFactory.decodeResource(resources, R.mipmap.guaguaka_text)
val customDrawable = CustomDrawable(bitmap)
iv.setImageDrawable(customDrawable)
效果如下

從效果圖中可以看到,雖然我們將Bitmap縮放為整個(gè)邊界大小,但是Drawable并沒有覆蓋整個(gè)ImageView,這又是為什么呢?
在這里我們使用setImageDrawable()設(shè)置數(shù)據(jù),和在XML中給ImageView設(shè)置
android:src="@mipmap/avator"一樣都是給ImageView設(shè)置源圖像,而源圖像的大小和scaleType相關(guān),我們這里設(shè)置的ScaleType為center,所以ImageView必然會居中縮放圖片,然后將圖片的顯示位置通過setBounds()函數(shù)設(shè)置給CustomDrawable。也就是說setBounds()創(chuàng)建的畫布大小和ScaleType相關(guān),下面看下不同scaleType,顯示的效果。

很明顯,除了fitXY以外的模式下,ImageView會根據(jù)CustomDrawable的getIntrinsicHeight()、getIntrinsicWidth()中返回的寬高對Drawable進(jìn)行等比拉伸,以適配ImageView。在計(jì)算出CustomDrawable的位置后,通過setBounds()函數(shù)傳遞給CustomDrawable顯示。
4.3、setBackground(drawable)
下面使用setBackground(drawable)的方式來看下,此種方式如何計(jì)算出setBounds()的邊界?
val bitmap = BitmapFactory.decodeResource(resources, R.mipmap.avator)
val customDrawable = CustomDrawable(bitmap)
tv.background=customDrawable
XML布局文件如下
<TextView
android:id="@+id/tv"
android:layout_width="wrap_content"
android:layout_height="wrap_content" />
效果圖如下

從效果圖中可以明顯看出,寬度使用的是TextView的寬度,高度使用的是Drawable的高度。
之所以會出現(xiàn)這樣的效果,是因?yàn)樵谑褂胹etBackground()設(shè)置自定義Drawable時(shí),控件的寬高計(jì)算會將將自定義Drawable的寬高和View的寬高進(jìn)行比較,取最大值??丶膶捀叽_定后,然后通過setBounds()將控件所在的矩形區(qū)域設(shè)置給自定義Drawable。
正式由于setBackground()函數(shù)計(jì)算寬高特性,所以有時(shí)候我們不希望改變控件的wrap_content特性,也就是讓控件的寬高以自己的寬高為準(zhǔn),而不考慮Drawable的寬高。解決這個(gè)問題,很簡單,在在定義Drawable時(shí)不重寫getIntrinsicHeight()、getIntrinsicWidth()即可,默認(rèn)返回-1。效果如下:

總結(jié):
- 1、當(dāng)使用setImageDrawable(drawable)函數(shù)設(shè)置ImageView數(shù)據(jù)源時(shí),會根據(jù)scaleType、ImageView控件的寬高、自定義Drawable的默認(rèn)寬高,對Drawable進(jìn)行縮放以適應(yīng)ImageView,自定義Drawable的位置和大小和scaleType相關(guān)。
- 2、當(dāng)使用setBackground()設(shè)置View的背景時(shí),自定義Drawable的寬高和控件的寬高一致,當(dāng)控件的寬高為wrap_content時(shí),則會選取控件的寬高和自定義Drawable寬高的最大值,作為控件的寬高。