美文网首页
ShapeDrawable

ShapeDrawable

作者: code希必地 | 来源:发表于2020-12-20 15:48 被阅读0次

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

    效果图如下


    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 {
        }
    }
    
    • 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控件

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

    这里两点需要注意:

    • 1、设置ImageView的背景为紫色。
    • 2、设置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
    总结:
    • 1、当使用setImageDrawable(drawable)函数设置ImageView数据源时,会根据scaleType、ImageView控件的宽高、自定义Drawable的默认宽高,对Drawable进行缩放以适应ImageView,自定义Drawable的位置和大小和scaleType相关。
    • 2、当使用setBackground()设置View的背景时,自定义Drawable的宽高和控件的宽高一致,当控件的宽高为wrap_content时,则会选取控件的宽高和自定义Drawable宽高的最大值,作为控件的宽高。

    相关文章

      网友评论

          本文标题:ShapeDrawable

          本文链接:https://www.haomeiwen.com/subject/dlpxnktx.html