Explain in detail how to use 13 kinds of Drawable in Android

foreword

Regarding custom View, I believe everyone is already familiar with it. Today, I want to share a part about customizing View, which is customizing Drawable.

Drawable is an abstract class of drawable objects. Compared with View, it is more pure. It is only used to handle drawing-related work and not to deal with user interaction events, so it is suitable for handling background drawing.

Before introducing custom Drawables, let's learn about several common Drawables.

Drawable resource introduction

A drawable object refers to a graphic that can be drawn on the screen, which can be obtained by methods such as getDrawable(int), and then applied to attribute methods such as android:drawable and android:icon.

The following introduces several common drawable objects, and I will introduce them in three steps:

1. Introduce how to use it in XML (with examples).

2. Then introduce its attribute method.

3. Then dynamically realize the same effect in XML in the form of code (an example will be given).

BitmapDrawable

bitmap image. Android supports bitmap files in three formats: .png (preferred), .jpg (acceptable), .gif (not recommended). We can directly use the file name as the resource ID to refer to the bitmap file, or create an alias resource ID in the XML file, which is called an XML bitmap.

XML bitmap: defined by an XML file, pointing to the bitmap file, the file is located in res/drawable/filename.xml, and its file name is the referenced resource ID, such as: R.drawable.filename.

About the <bitmap> attribute:

1. android:src : references drawable object resources, required.

2. android:tileMode : Define the tile mode. When tiled mode is enabled, the bitmap is repeated, and note: once tiled mode is enabled, the android:gravity attribute will be ignored.

The value defining the tile property must be one of the following:

disabled : Do not tile the bitmap, default value.

clamp : Copies the edge color when the shader draws beyond its original bounds.

repeat : Repeats the shader's image horizontally and vertically.

mirror : Repeats the shader's image horizontally and vertically, mirroring images alternately so that adjacent images always touch.

Note: The android:gravity attribute will be ignored when tiled mode is enabled.

android:gravity : Defines the gravity property of the bitmap. When the bitmap is smaller than the container, the position where the drawable object is placed in its container.

top : places the object on top of its container without changing its size.

bottom : places the object at the bottom of its container without changing its size.

left : places the object on the left edge of its container without changing its size.

right : places the object on the right edge of its container without changing its size.

center_vertical : places the object in the vertical center of its container without changing its size.

fill_vertical : Expands the vertical size of the object as needed to completely fit its container.

center_horizontal : places the object in the horizontal center of its container without changing its size.

fill_horizontal : Expands the object's horizontal size as needed to completely fit its container.

center : Centers the object on the horizontal and vertical axes of its container without changing its size.

fill : Expands the vertical size of the object as needed to completely fit its container. It's the default value.

clip_vertical : Can be set as an additional option to have the top and/or bottom edges of child elements clip to the bounds of their container. Cropping is based on vertical gravity: top gravity clips the top edge, bottom gravity clips the bottom edge, neither gravity will clip both sides at the same time.

clip_horizontal : Can be set as an additional option to have the child element's left and/or right clip to its container's bounds. Cropping is based on horizontal gravity: left gravity clips the right edge, right gravity clips the left edge, neither gravity will clip both sides at the same time.

In addition to defining bitmaps in XML files, we can also implement them directly through code, that is, BitmapDrawable.

val bitmap = BitmapFactory.decodeResource(resources, R.drawable.nick)
val bitmapShape = BitmapDrawable(resources, bitmap)
binding.tv2.background = bitmapShape

The effect diagram is as follows:

LayerDrawable

Layer list (LayerDrawable): It is a drawable object composed of a list of drawable objects. Each drawable in the list is drawn in list order, with the last drawable in the list drawn on top.

Each drawable is represented by an <item> element within a single <layer-list> element.

Introduce the properties:

1. <layer-list> : A mandatory root element. Contains one or more <item> elements.

2. <item> : It is a sub-item of the <layer-list> element, and its attributes support the definition of the position in the layer.

android:drawable:必备。引用可绘制对象资源。

android:top:整型。顶部偏移(像素)。

android:right:整型。右边偏移(像素)。

android:bottom:整型。底部偏移(像素)。

android:left:整型。左边偏移(像素)。

除了通过在XML中实现,我们同样可以通过代码来实现上面同样的效果。

val itemLeft = GradientDrawable().apply {
    setColor(ContextCompat.getColor(requireContext(), R.color.royal_blue))
    setSize(50.px, 50.px)
    shape = GradientDrawable.OVAL
}
val itemCenter = GradientDrawable().apply {
    setColor(ContextCompat.getColor(requireContext(), R.color.indian_red))
    shape = GradientDrawable.OVAL
}
val itemRight = GradientDrawable().apply {
    setColor(ContextCompat.getColor(requireContext(), R.color.yellow))
    shape = GradientDrawable.OVAL
}
val arr = arrayOf(
    ContextCompat.getDrawable(requireContext(), R.drawable.nick)!!,
    itemLeft,
    itemCenter,
    itemRight
)
val ld = LayerDrawable(arr).apply {
    setLayerInset(1, 0.px, 0.px, 250.px, 150.px)
    setLayerInset(2, 125.px, 75.px, 125.px, 75.px)
    setLayerInset(3, 250.px, 150.px, 0.px, 0.px)
}
binding.tv2.background = ld

效果图如下所示:

StateListDrawable

状态列表(StateListDrawable):会根据对象状态,使用多个不同的图像来表示同一个图形。

介绍一下其中的属性:

<selector>:必备的根元素。包含一个或多个 <item> 元素。

<item>:定义在某些状态期间使用的可绘制对象,必须是 <selector> 元素的子项。

其属性:

android:drawable:引用可绘制对象资源,必备。

android:state_pressed:布尔值。是否按下对象(例如触摸/点按某按钮)。

android:state_checked:布尔值。是否选中对象。

android:state_enabled:布尔值。是否能够接收触摸或点击事件。

除了通过在XML中实现,我们同样可以通过代码来实现上面同样的效果。

val sld = StateListDrawable().apply {
    addState(
        intArrayOf(android.R.attr.state_pressed),
        ContextCompat.getDrawable(requireContext(), R.drawable.basketball)
    )
    addState(StateSet.WILD_CARD, ContextCompat.getDrawable(requireContext(), R.drawable.nick))
}
binding.stateListDrawableTv2.apply {
    background = sld
    setOnClickListener {
        Log.e(TAG, "stateListDrawableTv2: isPressed = $isPressed")
    }
}

LevelListDrawable

级别列表(LevelListDrawable):管理可绘制对象列表,每个可绘制对象都有设置Level等级限制,当使用setLevel()时,会加载级别列表中 android:maxLevel 值大于或等于传递至方法的值的可绘制对象资源。

介绍一下其中的属性:

1. <level-list>:必备的根元素。包含一个或多个 <item> 元素。

2. <item>:在特定级别下使用的可绘制对象。

android:drawable:必备。引用可绘制对象资源。

android:maxLevel:整型。表示该Item允许的最高级别。

android:minLevel:整型。表示该Item允许的最低级别。

在将该 Drawable 应用到 View 后,就可以通过 setLevel() 或 setImageLevel() 更改级别。

除了通过在XML中实现,我们同样可以通过代码来实现上面同样的效果。

class LevelListDrawableFragment : BaseFragment<FragmentLevelListDrawableBinding>() {

    private val lld by lazy {
        LevelListDrawable().apply {
            addLevel(0, 1, getDrawable(R.drawable.nick))
            addLevel(0, 2, getDrawable(R.drawable.tom1))
            addLevel(0, 3, getDrawable(R.drawable.tom2))
            addLevel(0, 4, getDrawable(R.drawable.tom3))
            addLevel(0, 5, getDrawable(R.drawable.tom4))
            addLevel(0, 6, getDrawable(R.drawable.tom5))
            addLevel(0, 7, getDrawable(R.drawable.tom6))
            addLevel(0, 8, getDrawable(R.drawable.tom7))
            addLevel(0, 9, getDrawable(R.drawable.tom8))
            addLevel(0, 10, getDrawable(R.drawable.tom9))
        }
    }

    private fun getDrawable(id: Int): Drawable {
        return (ContextCompat.getDrawable(requireContext(), id)
            ?: ContextCompat.getDrawable(requireContext(), R.drawable.nick)) as Drawable
    }

    private val levelListDrawable by lazy {
        ContextCompat.getDrawable(requireContext(), R.drawable.level_list_drawable)
    }

    override fun initView() {

        binding.levelListDrawableInclude.apply {
            tv1.setText(R.string.level_list_drawable)
            tv1.background = levelListDrawable
            tv2.setText(R.string.level_list_drawable)

            tv2.background = lld
        }

        binding.seekBar.apply {
            //init level
            levelListDrawable?.level = progress
            lld.level = progress
            //add listener
            setOnSeekBarChangeListener(object : SeekBar.OnSeekBarChangeListener {
                override fun onProgressChanged(
                    seekBar: SeekBar?,
                    progress: Int,
                    fromUser: Boolean
                ) {
                    levelListDrawable?.level = progress
                    lld.level = progress
                    Log.e(TAG, "onProgressChanged: progreess = $progress")
                }

                override fun onStartTrackingTouch(seekBar: SeekBar?) {

                }

                override fun onStopTrackingTouch(seekBar: SeekBar?) {

                }
            })
        }

    }


}

效果图如下所示:

TransitionDrawable

转换可绘制对象(TransitionDrawable):可在两种可绘制对象资源之间交错淡出。

介绍一下其中的属性:

1. <transition>:必备的根元素。包含一个或多个 <item> 元素。

2. <item>:转换部分的可绘制对象。

android:drawable:必备。引用可绘制对象资源。

android:top、android:bottom、android:left、android:right:整型。偏移量(像素)。

注意:不能超过两个Item,调用 startTransition() 向前转换,调用 reverseTransition() 向后转换。

除了通过在XML中实现,我们同样可以通过代码来实现上面同样的效果。

class TransitionDrawableFragment : BaseFragment<FragmentTransitionDrawableBinding>() {

    private var isShow = false
    private lateinit var manualDrawable: TransitionDrawable

    override fun initView() {
        binding.transitionDrawableInclude.apply {
            val drawableArray = arrayOf(
                ContextCompat.getDrawable(requireContext(), R.drawable.nick),
                ContextCompat.getDrawable(requireContext(), R.drawable.basketball)
            )
            manualDrawable = TransitionDrawable(drawableArray)
            tv2.background = manualDrawable
        }
    }

    private fun setTransition() {
        if (isShow) {
            manualDrawable.reverseTransition(3000)
        } else {
            manualDrawable.startTransition(3000)
        }
    }

    override fun onResume() {
        super.onResume()
        setTransition()
        isShow = !isShow
    }

}

效果图如下所示:

InsetDrawable

插入可绘制对象(InsetDrawable):以指定距离插入其他可绘制对象,当视图需要小于视图实际边界的背景时,此类可绘制对象很有用。

介绍一下其属性:

<inset>:必备。根元素。

android:drawable:必备。引用可绘制对象资源。

android:insetTop、android:insetBottom、android:insetLeft、android:insetRight:尺寸。插入的,表示为尺寸

除了通过在XML中实现,我们同样可以通过代码来实现上面同样的效果。

val insetDrawable = InsetDrawable(
    ContextCompat.getDrawable(requireContext(), R.drawable.nick),
    0f, 0f, 0.5f, 0.25f
)
binding.tv2.background = insetDrawable

效果图如下所示:

ClipDrawable

裁剪可绘制对象(ClipDrawable):根据level等级对可绘制对象进行裁剪,可以根据level与gravity来控制子可绘制对象的宽度与高度。

<?xml version="1.0" encoding="utf-8"?>
<clip xmlns:android="http://schemas.android.com/apk/res/android"
    android:drawable="@drawable/nick"
    android:clipOrientation="horizontal"
    android:gravity="center">

</clip>

介绍一下其属性:

<clip>:必备。根元素。

android:drawable:必备。引用可绘制对象资源。

android:clipOrientation:裁剪方向。

horizontal:水平裁剪。

vertical:垂直裁剪。

android:gravity:重力属性。

最后通过设置level等级来实现裁剪,level 默认级别为 0,即完全裁剪,使图像不可见。当级别为 10,000 时,图像不会裁剪,而是完全可见。

除了通过在XML中实现,我们同样可以通过代码来实现上面同样的效果。

class ClipDrawableFragment : BaseFragment<FragmentClipDrawableBinding>() {

    private val clipDrawable by lazy {
        ContextCompat.getDrawable(requireContext(), R.drawable.clip_drawable)
    }
    private val manualClipDrawable by lazy {
        ClipDrawable(
            ContextCompat.getDrawable(requireContext(), R.drawable.nick),
            Gravity.CENTER,
            ClipDrawable.VERTICAL
        )
    }

    override fun initView() {
        binding.clipDrawableInclude.apply {
            tv1.setText(R.string.clip_drawable)
            tv1.background = clipDrawable
            tv2.setText(R.string.clip_drawable)
            tv2.background = manualClipDrawable
        }

        //level 默认级别为 0,即完全裁剪,使图像不可见。当级别为 10,000 时,图像不会裁剪,而是完全可见。
        binding.seekBar.apply {
            //init level
            clipDrawable?.level = progress
            manualClipDrawable.level = progress
            //add listener
            setOnSeekBarChangeListener(object : SeekBar.OnSeekBarChangeListener {
                override fun onProgressChanged(
                    seekBar: SeekBar?,
                    progress: Int,
                    fromUser: Boolean
                ) {
                    clipDrawable?.level = progress
                    manualClipDrawable.level = progress
                }

                override fun onStartTrackingTouch(seekBar: SeekBar?) {

                }

                override fun onStopTrackingTouch(seekBar: SeekBar?) {

                }

            })
        }
    }

}

效果图如下所示:

ScaleDrawable

缩放可绘制对象(ScaleDrawable):根据level等级来更改其可绘制对象大小。

<?xml version="1.0" encoding="utf-8"?>
<scale xmlns:android="http://schemas.android.com/apk/res/android"
    android:drawable="@drawable/nick"
    android:scaleWidth="100%"
    android:scaleHeight="100%"
    android:scaleGravity="center">

</scale>

介绍一下其属性:

<scale>:必备。根元素。

android:drawable:必备。引用可绘制对象资源。

android:scaleGravity:指定缩放后的重力位置。

android:scaleHeight:百分比。缩放高度,表示为可绘制对象边界的百分比。值的格式为 XX%。例如:100%、12.5% 等。

android:scaleWidth:百分比。缩放宽度,表示为可绘制对象边界的百分比。值的格式为 XX%。例如:100%、12.5% 等。

除了通过在XML中实现,我们同样可以通过代码来实现上面同样的效果。

val scaleDrawable = ScaleDrawable(
    ContextCompat.getDrawable(requireContext(), R.drawable.nick),
    Gravity.CENTER,
    1f,
    1f
)
binding.tv2.background = scaleDrawable

binding.seekBar.apply {
    //init level
    tv1.background.level = progress
    scaleDrawable.level = progress
    //add listener
    setOnSeekBarChangeListener(object : SeekBar.OnSeekBarChangeListener {
        override fun onProgressChanged(
            seekBar: SeekBar?,
            progress: Int,
            fromUser: Boolean
        ) {
            tv1.background.level = progress
            scaleDrawable.level = progress
            Log.e(TAG, "onProgressChanged: progreess = $progress")
        }

        override fun onStartTrackingTouch(seekBar: SeekBar?) {

        }

        override fun onStopTrackingTouch(seekBar: SeekBar?) {

        }

    })
}

效果图如下所示:

ShapeDrawable

形状可绘制对象(ShapeDrawable):通过XML来定义各种形状的可绘制对象。

介绍一下其属性:

1. <shape>:必备。根元素。

2. android:shape:定义形状的类型。

rectangle:默认形状,填充包含视图的矩形。

oval:适应包含视图尺寸的椭圆形状。

line:跨越包含视图宽度的水平线。此形状需要 元素定义线宽。

ring:环形。

android:innerRadius:尺寸。环内部(中间的孔)的半径。

android:thickness:尺寸。环的厚度。

3. <corners>:圆角,仅当形状为矩形时适用。

android:radius:尺寸。所有角的半径。如果想要设置单独某个角,可以使用android:topLeftRadius、android:topRightRadius、android:bottomLeftRadius、android:bottomRightRadius。

4. <padding>:设置内边距。

android:left:尺寸。设置左内边距。同样还有android:right、android:top、android:bottom供选择。

5. <size>:形状的大小。

android:height:尺寸。形状的高度。

android:width:尺寸。形状的宽度。

6. <solid>:填充形状的纯色。

android:color:颜色。

7. <stroke>:形状的笔画

android:width:尺寸。线宽。

android:color:颜色。线的颜色。

android:dashGap:尺寸。短划线的间距。虚线效果。

android:dashWidth:尺寸。每个短划线的大小。虚线效果。

除了通过在XML中实现,我们同样可以通过代码来实现上面同样的效果。

class ShapeDrawableFragment : BaseFragment<FragmentShapeDrawableBinding>() {

    override fun initView() {
        val roundRectShape =
            RoundRectShape(
                floatArrayOf(20f.px, 20f.px, 20f.px, 20f.px, 0f, 0f, 0f, 0f),
                null,
                null
            )
        binding.tv2.background = MyShapeDrawable(roundRectShape)
    }

    /**
     * TODO: 使用 GradientDrawable 效果更好
     */
    class MyShapeDrawable(shape: Shape) : ShapeDrawable(shape) {
        private val fillPaint = Paint().apply {
            style = Paint.Style.FILL
            color = Color.parseColor("#4169E1")
        }
        private val strokePaint = Paint().apply {
            style = Paint.Style.STROKE
            color = Color.parseColor("#FFBB86FC")
            strokeMiter = 10f
            strokeWidth = 5f.px
            pathEffect = DashPathEffect(floatArrayOf(10f.px, 5f.px), 0f)
        }

        override fun onDraw(shape: Shape?, canvas: Canvas?, paint: Paint?) {
            super.onDraw(shape, canvas, paint)
            shape?.draw(canvas, fillPaint)
            shape?.draw(canvas, strokePaint)
        }
    }

}

效果图如下所示:

GradientDrawable

渐变可绘制对象(GradientDrawable):如其名,实现渐变颜色效果。其实也是属于ShapeDrawable。

介绍一下其属性:

1. <shape>:必备。根元素。

2. gradient:表示渐变的颜色。

android:angle:整型。表示渐变的角度。0 表示为从左到右,90 表示为从上到上。注意:必须是 45 的倍数。默认值为 0。

android:centerX:浮点型。表示渐变中心相对 X 轴位置 (0 - 1.0)。android:centerY同理。

android:startColor:颜色。起始颜色。android:endColor、android:centerColor分别表示结束颜色与中间颜色。

android:gradientRadius:浮点型。渐变的半径。仅在 android:type="radial" 时适用。

android:type:渐变的类型。

linear:线性渐变。默认为该类型。

radial:径向渐变,也就是雷达式渐变,起始颜色为中心颜色。

sweep:流线型渐变。

除了通过在XML中实现,我们同样可以通过代码来实现上面同样的效果。

val gradientDrawable = GradientDrawable().apply {
    shape = GradientDrawable.OVAL
    gradientType = GradientDrawable.RADIAL_GRADIENT
    colors = intArrayOf(Color.parseColor("#00F5FF"), Color.parseColor("#BBFFFF"))
    gradientRadius = 100f.px
}
binding.tv2.background = gradientDrawable

效果图如下所示:

AnimationDrawable

动画可绘制对象(AnimationDrawable):用于创建逐帧动画的可绘制对象。

<?xml version="1.0" encoding="utf-8"?>
<animation-list xmlns:android="http://schemas.android.com/apk/res/android">
    <item
        android:drawable="@drawable/nick"
        android:duration="1000" />
    <item
        android:drawable="@drawable/basketball"
        android:duration="1000" />

</animation-list>

介绍一下其属性:

1. <animation-list>:必备。根元素。

2. <item>:每一帧的可绘制对象。

android:drawable:必备。引用可绘制对象资源。

android:duration:该帧的持续时间,单位为毫秒。

android:oneshot:布尔值。代表是否只单次展示该动画,默认为false。

除了通过在XML中实现,我们同样可以通过代码来实现上面同样的效果。

val animationDrawable = AnimationDrawable().apply {
    ContextCompat.getDrawable(requireContext(), R.drawable.nick)
        ?.let { addFrame(it, 1000) }
    ContextCompat.getDrawable(requireContext(), R.drawable.basketball)
        ?.let { addFrame(it, 1000) }
}
binding.tv2.background = animationDrawable
animationDrawable.start()

效果图如下所示:

自定义 Drawable

介绍完了几种常见的可绘制对象资源,接下来我们进一步学习一下,如果进行自定义Drawable。

class JcTestDrawable : Drawable() {

    override fun draw(p0: Canvas) {
        TODO("Not yet implemented")
    }

    override fun setAlpha(p0: Int) {
        TODO("Not yet implemented")
    }

    override fun setColorFilter(p0: ColorFilter?) {
        TODO("Not yet implemented")
    }

    override fun getOpacity(): Int {
        TODO("Not yet implemented")
    }

}

从上述代码可以看出,我们需要继承Drawable(),然后实现4个方法,分别是:

1. setAlpha:为Drawable指定一个alpha值,0 表示完全透明,255 表示完全不透明。

2. setColorFilter:为Drawable指定可选的颜色过滤器。Drawable的draw绘图内容的每个输出像素在混合到 Canvas 的渲染目标之前将被颜色过滤器修改。传递 null 会删除任何现有的颜色过滤器。

3. getOpacity:返回Drawable的透明度,如下所示:

PixelFormat.TRANSLUCENT:半透明的。

PixelFormat.TRANSPARENT:透明的。

PixelFormat.OPAQUE:不透明的。

PixelFormat.UNKNOWN:未知。

4. draw:在边界内进行绘制(通过setBounds()),受alpha与colorFilter所影响。

接下来为大家举个例子。

举例:滚动篮球

功能介绍:当我们点击屏幕,篮球会滚向该坐标。

如下图所示:

实现步骤可以简单分为两步:

1. 绘制一个篮球。

2.获取到用户点击坐标,使用属性动画让篮球滚动到该位置。

绘制篮球

首先说绘制篮球这一步,这一步不需要与用户进行交互,所以我们采用自定义Drawable来进行绘制。

如下所示:

class BallDrawable : Drawable() {
    private val paint = Paint(Paint.ANTI_ALIAS_FLAG).apply {
        style = Paint.Style.FILL
        color = Color.parseColor("#D2691E")
    }

    private val linePaint = Paint(Paint.ANTI_ALIAS_FLAG).apply {
        style = Paint.Style.STROKE
        strokeWidth = 1f.px
        color = Color.BLACK
    }

    override fun draw(canvas: Canvas) {
        val radius = bounds.width().toFloat() / 2
        canvas.drawCircle(
            bounds.width().toFloat() / 2,
            bounds.height().toFloat() / 2,
            radius,
            paint
        )

        //the vertical line of the ball
        canvas.drawLine(
            bounds.width().toFloat() / 2,
            0f,
            bounds.width().toFloat() / 2,
            bounds.height().toFloat(),
            linePaint
        )
        //the transverse line of the ball
        canvas.drawLine(
            0f,
            bounds.height().toFloat() / 2,
            bounds.width().toFloat(),
            bounds.height().toFloat() / 2,
            linePaint
        )

        val path = Path()
        val sinValue = kotlin.math.sin(Math.toRadians(45.0)).toFloat()
        //left curve
        path.moveTo(radius - sinValue * radius,
            radius - sinValue * radius
        )
        path.cubicTo(radius - sinValue * radius,
            radius - sinValue * radius,
            radius,
            radius,
            radius - sinValue * radius,
            radius + sinValue * radius
        )
        //right curve
        path.moveTo(radius + sinValue * radius,
            radius - sinValue * radius
        )
        path.cubicTo(radius + sinValue * radius,
            radius - sinValue * radius,
            radius,
            radius,
            radius + sinValue * radius,
            radius + sinValue * radius
        )
        canvas.drawPath(path, linePaint)
    }

    override fun setAlpha(alpha: Int) {
        paint.alpha = alpha
    }

    override fun getOpacity(): Int {
        return when (paint.alpha) {
            0xff -> PixelFormat.OPAQUE
            0x00 -> PixelFormat.TRANSPARENT
            else -> PixelFormat.TRANSLUCENT
        }
    }

    override fun setColorFilter(colorFilter: ColorFilter?) {
        paint.colorFilter = colorFilter
    }
}

滚动

绘制好篮球后,接着就是获取到用户的点击坐标,为了更好的举例,这里我放在自定义View中进行完成。

如下所示:

class CustomBallMovingSiteView(context: Context, attributeSet: AttributeSet?, defStyleAttr: Int) :
    FrameLayout(context, attributeSet, defStyleAttr) {

    constructor(context: Context) : this(context, null, 0)
    constructor(context: Context, attributeSet: AttributeSet?) : this(context, attributeSet, 0)

    private lateinit var ballContainerIv: ImageView
    private val ballDrawable = BallDrawable()
    private val radius = 50

    private var rippleAlpha = 0
    private var rippleRadius = 10f

    private var rawTouchEventX = 0f
    private var rawTouchEventY = 0f
    private var touchEventX = 0f
    private var touchEventY = 0f
    private var lastTouchEventX = 0f
    private var lastTouchEventY = 0f

    private val ripplePaint = Paint(Paint.ANTI_ALIAS_FLAG).apply {
        isDither = true
        color = Color.RED
        style = Paint.Style.STROKE
        strokeWidth = 2f.px
        alpha = rippleAlpha
    }

    init {
        initView(context, attributeSet)
    }

    private fun initView(context: Context, attributeSet: AttributeSet?) {
        //generate a ball by dynamic
        ballContainerIv = ImageView(context).apply {
            layoutParams = LayoutParams(radius * 2, radius * 2).apply {
                gravity = Gravity.CENTER
            }

            setImageDrawable(ballDrawable)
            //setBackgroundColor(Color.BLUE)
        }

        addView(ballContainerIv)
        setWillNotDraw(false)
    }

    override fun onTouchEvent(event: MotionEvent?): Boolean {
        lastTouchEventX = touchEventX
        lastTouchEventY = touchEventY

        event?.let {
            rawTouchEventX = it.x
            rawTouchEventY = it.y
            touchEventX = it.x - radius
            touchEventY = it.y - radius
        }

        ObjectAnimator.ofFloat(this, "rippleValue", 0f, 1f).apply {
            duration = 1000
            start()
        }

        val path = Path().apply {
            moveTo(lastTouchEventX, lastTouchEventY)
            quadTo(
                lastTouchEventX,
                lastTouchEventY,
                touchEventX,
                touchEventY
            )
        }

        val oaMoving = ObjectAnimator.ofFloat(ballContainerIv, "x", "y", path)
        val oaRotating = ObjectAnimator.ofFloat(ballContainerIv, "rotation", 0f, 360f)

        AnimatorSet().apply {
            duration = 1000
            playTogether(oaMoving, oaRotating)
            start()
        }

        return super.onTouchEvent(event)
    }

    fun setRippleValue(currentValue: Float) {
        rippleRadius = currentValue * radius
        rippleAlpha = ((1 - currentValue) * 255).toInt()
        invalidate()
    }

    override fun onDraw(canvas: Canvas?) {
        super.onDraw(canvas)
        ripplePaint.alpha = rippleAlpha
        //draw ripple for click event
        canvas?.drawCircle(rawTouchEventX, rawTouchEventY, rippleRadius, ripplePaint)
    }
}

简单概括一下:首先我们会动态的生成一个View,将其背景设置为我们刚刚绘制的BallDrawable()来构成一个篮球。然后通过onTouchEvent()方法来获取到用户的点击坐标,再通过属性动画,让球滚动到该坐标。

更多额外代码请查看 Github Drawable_Leaning 之篮球滚动

https://github.com/JereChen11/Drawable_Learning/tree/main/app/src/main/java/com/drawable/learning/fragment/custom/ball

总结

通过这篇文章我们学习了几种常见的Drawable,也学习了自定义Drawable,我们知道Drawable只用来处理绘制的相关工作而不处理与用户的交互事件。所以,在我们复杂的自定义View中,我们可以将其进行拆分,像一些背景、装饰等完全就可以采取自定义Drawable来进行绘制。这样就能让我们复杂的自定义View变得图层更加层次清晰,代码可读性大大提升。

如果你想参考文章中所有源码,可以点击 Github Drawable_Learning 进行查看,欢迎你给我点个小星星。

https://github.com/JereChen11/Drawable_Learning

Guess you like

Origin blog.csdn.net/qq_39312146/article/details/129218190