ShapeDrawable

2020-12-20  本文已影响0人  code希必地

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的派生类有如下几个:

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

}

在上面示例中,我们做了如下几件事:

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

}

效果图如下


image.png

我们通过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、放大镜效果

先看下效果图


放大镜.gif

这里会使用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 {
    }
}
  • 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即可。
在这里又多写了几个函数:

4.2、setImageDrawable(drawable)

我们在布局中定义一个ImageView控件

<ImageView
    android:id="@+id/iv"
    android:layout_width="200dp"
    android:layout_height="100dp"
    android:background="@color/purple_200"
    android:scaleType="center" />

这里两点需要注意:

val bitmap = BitmapFactory.decodeResource(resources, R.mipmap.guaguaka_text)
val customDrawable = CustomDrawable(bitmap)
iv.setImageDrawable(customDrawable)

效果如下

image.png
从效果图中可以看到,虽然我们将Bitmap缩放为整个边界大小,但是Drawable并没有覆盖整个ImageView,这又是为什么呢?
在这里我们使用setImageDrawable()设置数据,和在XML中给ImageView设置android:src="@mipmap/avator"一样都是给ImageView设置源图像,而源图像的大小和scaleType相关,我们这里设置的ScaleType为center,所以ImageView必然会居中缩放图片,然后将图片的显示位置通过setBounds()函数设置给CustomDrawable。
也就是说setBounds()创建的画布大小和ScaleType相关,下面看下不同scaleType,显示的效果。
image.png
很明显,除了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
    android:id="@+id/tv"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content" />

效果图如下


image.png

从效果图中可以明显看出,宽度使用的是TextView的宽度,高度使用的是Drawable的高度。
之所以会出现这样的效果,是因为在使用setBackground()设置自定义Drawable时,控件的宽高计算会将将自定义Drawable的宽高和View的宽高进行比较,取最大值。控件的宽高确定后,然后通过setBounds()将控件所在的矩形区域设置给自定义Drawable。
正式由于setBackground()函数计算宽高特性,所以有时候我们不希望改变控件的wrap_content特性,也就是让控件的宽高以自己的宽高为准,而不考虑Drawable的宽高。解决这个问题,很简单,在在定义Drawable时不重写getIntrinsicHeight()、getIntrinsicWidth()即可,默认返回-1。效果如下:


image.png
总结:
上一篇 下一篇

猜你喜欢

热点阅读