ShapeDrawable

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)
    }

}

效果圖如下


image.png

我們通過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、放大鏡效果

先看下效果圖


放大鏡.gif

這里會使用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)

效果如下

image.png

從效果圖中可以看到,雖然我們將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,顯示的效果。
image.png

很明顯,除了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" />

效果圖如下


image.png

從效果圖中可以明顯看出,寬度使用的是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。效果如下:


image.png

總結(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寬高的最大值,作為控件的寬高。
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容