详解Android 13种 Drawable的使用方法
前言
关于自定义View,相信大家都已经很熟悉了。今天,我想分享一下关于自定义View中的一部分,就是自定义Drawable。
Drawable 是可绘制对象的一个抽象类,相对比View来说,它更加的纯粹,只用来处理绘制的相关工作而不处理与用户的交互事件,所以适合用来处理背景的绘制。
在介绍自定义Drawable前,我们先来学习一下几种常见的Drawable。
可绘制对象资源介绍
可绘制对象是指可在屏幕上绘制的图形,可以通过getDrawable(int)等方法来获取,然后应用到 android:drawable 和 android:icon 等属性方法中。
下面介绍几种常见的可绘制对象,我会分三个步骤来介绍:
1. 介绍一下在XML中的使用方法(会举例说明)。
2. 然后介绍一下其属性方法。
3. 再以代码的形式来动态实现XML中的同样效果(会举例说明)。
BitmapDrawable
位图图像。Android支持三种格式的位图文件:.png(首选)、.jpg(可接受)、.gif(不建议)。我们可以直接使用文件名作为资源 ID 来引用位图文件,也可以在 XML 文件中创建别名资源 ID,这就叫做 XML位图。
XML位图:通过XML文件来定义,指向位图文件,文件位于res/drawable/filename.xml,其文件名就是作为引用的资源 ID,如:R.drawable.filename。

关于 <bitmap> 属性:
1. android:src:引用可绘制对象资源,必备。
2. android:tileMode:定义平铺模式。当平铺模式启用时,位图会重复,且注意:一旦平铺模式启用, android:gravity 属性就将会被忽略。
定义平铺属性的值必须是以下值之一:
• disabled:不平铺位图,默认值。
• clamp:当着色器绘制范围超出其原边界时复制边缘颜色。
• repeat:水平和垂直重复着色器的图像。
• mirror:水平和垂直重复着色器的图像,交替镜像图像以使相邻图像始终相接。
注意:在平铺模式启用时 android:gravity 属性将被忽略。
android:gravity:定义位图的重力属性,当位图小于容器时,可绘制对象在其容器中放置的位置。
• top:将对象放在其容器顶部,不改变其大小。
• bottom:将对象放在其容器底部,不改变其大小。
• left:将对象放在其容器左边缘,不改变其大小。
• right:将对象放在其容器右边缘,不改变其大小。
• center_vertical:将对象放在其容器的垂直中心,不改变其大小。
• fill_vertical:按需要扩展对象的垂直大小,使其完全适应其容器。
• center_horizontal:将对象放在其容器的水平中心,不改变其大小。
• fill_horizontal:按需要扩展对象的水平大小,使其完全适应其容器。
• center:将对象放在其容器的水平和垂直轴中心,不改变其大小。
• fill:按需要扩展对象的垂直大小,使其完全适应其容器。这是默认值。
• clip_vertical:可设置为让子元素的上边缘和/或下边缘裁剪至其容器边界的附加选项。裁剪基于垂直重力:顶部重力裁剪上边缘,底部重力裁剪下边缘,任一重力不会同时裁剪两边。
• clip_horizontal:可设置为让子元素的左边和/或右边裁剪至其容器边界的附加选项。裁剪基于水平重力:左边重力裁剪右边缘,右边重力裁剪左边缘,任一重力不会同时裁剪两边。
除了在 XML 文件中定义位图,我们也可以直接通过代码来实现,即BitmapDrawable。
val bitmap = BitmapFactory.decodeResource(resources, R.drawable.nick)
val bitmapShape = BitmapDrawable(resources, bitmap)
binding.tv2.background = bitmapShape效果图如下所示:

LayerDrawable
图层列表(LayerDrawable):是可绘制对象列表组成的可绘制对象。列表中的每个可绘制对象均按照列表顺序绘制,列表中的最后一个可绘制对象绘于顶部。
每个可绘制对象由单一 <layer-list> 元素内的 <item> 元素表示。

介绍一下其中的属性:
1. <layer-list>:必备的根元素。包含一个或多个 <item> 元素。
2. <item>:是 <layer-list> 元素的子项,其属性支持定义在图层中所处的位置。
• 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 = sldsetOnClickListener {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 = levelListDrawabletv2.setText(R.string.level_list_drawable)tv2.background = lld}binding.seekBar.apply {//init levellevelListDrawable?.level = progresslld.level = progress//add listenersetOnSeekBarChangeListener(object : SeekBar.OnSeekBarChangeListener {override fun onProgressChanged(seekBar: SeekBar?,progress: Int,fromUser: Boolean) {levelListDrawable?.level = progresslld.level = progressLog.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 = falseprivate lateinit var manualDrawable: TransitionDrawableoverride 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 = clipDrawabletv2.setText(R.string.clip_drawable)tv2.background = manualClipDrawable}//level 默认级别为 0,即完全裁剪,使图像不可见。当级别为 10,000 时,图像不会裁剪,而是完全可见。binding.seekBar.apply {//init levelclipDrawable?.level = progressmanualClipDrawable.level = progress//add listenersetOnSeekBarChangeListener(object : SeekBar.OnSeekBarChangeListener {override fun onProgressChanged(seekBar: SeekBar?,progress: Int,fromUser: Boolean) {clipDrawable?.level = progressmanualClipDrawable.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 = scaleDrawablebinding.seekBar.apply {//init leveltv1.background.level = progressscaleDrawable.level = progress//add listenersetOnSeekBarChangeListener(object : SeekBar.OnSeekBarChangeListener {override fun onProgressChanged(seekBar: SeekBar?,progress: Int,fromUser: Boolean) {tv1.background.level = progressscaleDrawable.level = progressLog.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.FILLcolor = Color.parseColor("#4169E1")}private val strokePaint = Paint().apply {style = Paint.Style.STROKEcolor = Color.parseColor("#FFBB86FC")strokeMiter = 10fstrokeWidth = 5f.pxpathEffect = 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.OVALgradientType = GradientDrawable.RADIAL_GRADIENTcolors = 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"><itemandroid:drawable="@drawable/nick"android:duration="1000" /><itemandroid: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.FILLcolor = Color.parseColor("#D2691E")}private val linePaint = Paint(Paint.ANTI_ALIAS_FLAG).apply {style = Paint.Style.STROKEstrokeWidth = 1f.pxcolor = Color.BLACK}override fun draw(canvas: Canvas) {val radius = bounds.width().toFloat() / 2canvas.drawCircle(bounds.width().toFloat() / 2,bounds.height().toFloat() / 2,radius,paint)//the vertical line of the ballcanvas.drawLine(bounds.width().toFloat() / 2,0f,bounds.width().toFloat() / 2,bounds.height().toFloat(),linePaint)//the transverse line of the ballcanvas.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 curvepath.moveTo(radius - sinValue * radius,radius - sinValue * radius)path.cubicTo(radius - sinValue * radius,radius - sinValue * radius,radius,radius,radius - sinValue * radius,radius + sinValue * radius)//right curvepath.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.OPAQUE0x00 -> PixelFormat.TRANSPARENTelse -> 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: ImageViewprivate val ballDrawable = BallDrawable()private val radius = 50private var rippleAlpha = 0private var rippleRadius = 10fprivate var rawTouchEventX = 0fprivate var rawTouchEventY = 0fprivate var touchEventX = 0fprivate var touchEventY = 0fprivate var lastTouchEventX = 0fprivate var lastTouchEventY = 0fprivate val ripplePaint = Paint(Paint.ANTI_ALIAS_FLAG).apply {isDither = truecolor = Color.REDstyle = Paint.Style.STROKEstrokeWidth = 2f.pxalpha = rippleAlpha}init {initView(context, attributeSet)}private fun initView(context: Context, attributeSet: AttributeSet?) {//generate a ball by dynamicballContainerIv = 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 = touchEventXlastTouchEventY = touchEventYevent?.let {rawTouchEventX = it.xrawTouchEventY = it.ytouchEventX = it.x - radiustouchEventY = it.y - radius}ObjectAnimator.ofFloat(this, "rippleValue", 0f, 1f).apply {duration = 1000start()}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 = 1000playTogether(oaMoving, oaRotating)start()}return super.onTouchEvent(event)}fun setRippleValue(currentValue: Float) {rippleRadius = currentValue * radiusrippleAlpha = ((1 - currentValue) * 255).toInt()invalidate()}override fun onDraw(canvas: Canvas?) {super.onDraw(canvas)ripplePaint.alpha = rippleAlpha//draw ripple for click eventcanvas?.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
相关文章:
详解Android 13种 Drawable的使用方法
前言关于自定义View,相信大家都已经很熟悉了。今天,我想分享一下关于自定义View中的一部分,就是自定义Drawable。Drawable 是可绘制对象的一个抽象类,相对比View来说,它更加的纯粹,只用来处理绘制的相关工作…...
MakeFile教程
前言 当我们需要编译一个比较大的项目时,编译命令会变得越来越复杂,需要编译的文件越来越多。其 次就是项目中并不是每一次编译都需要把所有文件都重新编译,比如没有被修改过的文件则不需要重 新编译。工程管理器就帮助我们来优化这两个问题…...
Spring使用mongoDB步骤
1. 在Linux系统使用docker安装mongoDB 1.1. 安装 在docker运行的情况下,执行下述命令。 docker run \ -itd \ --name mongoDB \ -v mongoDB_db:/data/db \ -p 27017:27017 \ mongo:4.4 \ --auth执行docker ps后,出现下列行,即表示mongoDB安…...
【蓝牙mesh】access层(接入层)协议介绍
【蓝牙mesh】access层(接入层)协议介绍 Access层简介 Access层定义了应用层如何使用upper协议层的接口,它不仅定义了应用层的格式,还定义了应用数据在upper层的加密和解密。当收到下层的数据包时,它会检查数据的netke…...
【一天一门编程语言】JavaScript 语言程序设计极简教程
JavaScript 语言程序设计极简教程 用 markdown 格式输出答案。 不少于3000字。细分到2级目录。 一、JavaScript 简介 1.1 什么是 JavaScript JavaScript 是一种由Netscape的LiveScript发展而来的脚本语言,是一种动态类型、弱类型、基于原型的语言,内…...
CMake调试器出炉:调试你的CMake脚本
Visual Studio 开发团队一直和 Kitware 紧密合作,致力于开发一个用于调试 CMake 脚本的调试器。 我们将继续这个工作,以便开发人员社区可以通过添加新功能和对其他 DAP 功能的支持来共同改进它。 我们很高兴地宣布,CMake 调试器的预览版现在…...
题解 # 二维矩阵最大矩形问题#
题目: 小明有一张N*M的方格纸,且部分小方格中涂了颜色,部分小方格还是空白。 给出N (2<Ns30)和M(2sMs30)的值,及每个小方格的状态((被涂了颜色小方格用数字1表示,空白小方格用数字0表示); 请…...
奔四的路上,依旧倔强的相信未来
本文首发于2022年12月31日 原标题: 奔四的路上,依旧倔强的相信未来!–我的2022年终总结 读大学那几年,一直保持着写日记和做计划的习惯,还记得大学毕业刚开始打工的时候,我的床头的墙上一定会画一张表,写上一个月的计划和一周的计划 计划也会有完不成的时候,但加深了…...
61 k8s + rancher + karmada容器化部署
文章目录 一、什么是rancher二、为什么使用rancher三、rancher安装1、细部介绍四、图形化操作1、执行2、补充五、 karmada1、官网2、细部介绍一、什么是rancher 1、Rancher 是一个 全栈式 的 Kubernetes 容器管理平台,为你提供在任何地方都能成功运行 Kubernetes 的工具。 二…...
Vue3的新特性变化,上手指南!
文章目录一、Vue3相比Vue2,更新了什么变化?二、Proxy 代理响应式原理三、组合式 API (Composition API)setup()函数:ref()函数reactive()函数组合式 setup 中使用 Props 父向子传递参数计算属性watch(数据监视)watchEffect&#x…...
OllyDbg
本文通过吾爱破解论坛上提供的OllyDbg版本为例,讲解该软件的使用方法 F2对鼠标所处的位置打下断点,一般表现为鼠标所属地址位置背景变红F3加载一个可执行程序,进行调试分析,表现为弹出打开文件框F4执行程序到光标处F5缩小还原当前…...
记一次键盘维修,最终修复
我的笔记本是华硕的K45VD,是我亲人在高二那年买的,之后就一直给我用,距今2023年已经差不多13年,它承载了太多记忆。在大学期间也给它升级,重要的零部件基本没问题。只在大学时加了8G内存和一个240G固态,换了…...
LeetCode 155.最小栈
设计一个支持 push ,pop ,top 操作,并能在常数时间内检索到最小元素的栈。实现 MinStack 类:MinStack() 初始化堆栈对象。void push(int val) 将元素val推入堆栈。void pop() 删除堆栈顶部的元素。int top() 获取堆栈顶部的元素。int getMin(…...
C++学习笔记-重载运算符和重载函数
重载的运算符是带有特殊名称的函数,函数名是由关键字 operator 和其后要重载的运算符符号构成的。与其他函数一样,重载运算符有一个返回类型和一个参数列表。 C 允许在同一作用域中的某个函数和运算符指定多个定义,分别称为函数重载和运算符重…...
Java —— JDBC
引入mysql链接 创建表格 Navicat查看建表代码双击要打开的表,右侧顶端点击ddl小方框 CREATE TABLE s (id int(6) NOT NULL,name varchar(20) COLLATE utf8_bin DEFAULT NULL,age int(11) DEFAULT NULL,gender varchar(2) COLLATE utf8_bin DEFAULT NULL,dept var…...
备战金三银四,熬夜半个月汇集大厂 Java 岗 1600 页面试真题
如果你不停地加班。却很少冒险,也很少学习,那你极大可能会陷入到内卷中。 为什么这么说呢?我们先来捋清楚「内卷」的概念: 「内卷化」简而言之就是:日复一日,越混越掉坑里。 所谓内卷化,指一种…...
9、面向对象、泛型与反射
目录一、构造函数二、继承与重写三、泛型四、反射1 - 反射的基本概念2 - 反射的基础数据类型3 - 反射APIa - 获取Type类型b - 获取struct成员变量的信息c - 获取struct成员方法的信息d - 获取函数的信息e - 判断类型是否实现了某接口五、reflect.Valuea - 空value判断b - 获取V…...
Python使用百度通用API进行翻译
想汉化StarUML这个软件,感觉工作量太大,想要用Python自动翻译。 结果网上找的一个个用不了,或者用一会儿就断。 于是自己手写了一个简单的,只有两个类:APIConfig和Translater 使用 demo my_api_config APIConfig(…...
JavaScript 弹窗
文章目录JavaScript 弹窗警告框确认框提示框换行JavaScript 弹窗 可以在 JavaScript 中创建三种消息框:警告框、确认框、提示框。 警告框 警告框经常用于确保用户可以得到某些信息。 当警告框出现后,用户需要点击确定按钮才能继续进行操作。 语法 wi…...
408复试day1
文章目录数据结构计算机组成原理操作系统计算机网络数据结构 深度优先遍历DFS: 首先访问图中起始顶点v,然后由v出发,访问与v邻接且未被访问的顶点v1,再访问与v1相邻的且未被访问的顶点v2……重复上述过程。当不能再继续向下访问时…...
ComfyUI-Manager终极指南:3个核心功能彻底解决AI工作流管理难题
ComfyUI-Manager终极指南:3个核心功能彻底解决AI工作流管理难题 【免费下载链接】ComfyUI-Manager ComfyUI-Manager is an extension designed to enhance the usability of ComfyUI. It offers management functions to install, remove, disable, and enable vari…...
诚信标签工厂端解决方案 适配俄标 CRPT 体系一体化技术方案
俄罗斯诚实标签依托 CRPT 体系执行强制管控,各类出口货品必须完成 Data Matrix 编码采集、格式转换、多层包装数据绑定,数据合规后方可通关流通。美妆食品、日化建材、玩具五金等品类包装形态差异较大,人工采集方式普遍存在识别精度不足、批量…...
告别网盘客户端!用Alist+RaiDrive把百度云盘变成电脑本地文件夹(保姆级图文教程)
用AlistRaiDrive实现网盘本地化管理的终极方案 你是否厌倦了电脑上安装多个网盘客户端,不仅占用系统资源,操作还繁琐割裂?每次上传下载文件都要在不同客户端间切换,效率低下。现在,通过Alist和RaiDrive的组合…...
37家金融客户紧急启用的DeepSeek扫描辅助加固包(含未公开API调用密钥策略)
更多请点击: https://kaifayun.com 第一章:DeepSeek漏洞扫描辅助的背景与战略价值 近年来,大模型在安全领域的应用正从辅助问答向深度协同防御演进。DeepSeek系列模型凭借其开源、高推理精度及强代码理解能力,成为构建智能化漏洞…...
阿波罗登月,不可能:读心术与影子叙事 ——不是向全世界展示登月,而是向全世界注射登月
阿波罗登月,不可能:读心术与影子叙事 ——不是向全世界展示登月,而是向全世界注射登月 Jianbing Zhu 1^{1}1 1^{1}1 ECT-OS-JiuHuaShan 文明实验室 ORCID: 0009-0006-8591-1891 DOI: 10.5281/zenodo.20373157 Email: ect-os-jiuhuashanzoho…...
别再只用鼠标了!用Leap Motion手势控制Unity游戏,保姆级配置避坑指南(2024版)
2024年Unity手势交互开发实战:Leap Motion从配置到游戏逻辑全解析在游戏开发领域,交互方式的创新往往能带来全新的体验。想象一下,玩家不再需要键盘鼠标,仅凭自然的手部动作就能操控游戏角色——这正是Leap Motion手势识别技术为U…...
LeaguePrank:5分钟打造个性化英雄联盟客户端,段位头像随心换!
LeaguePrank:5分钟打造个性化英雄联盟客户端,段位头像随心换! 【免费下载链接】LeaguePrank 项目地址: https://gitcode.com/gh_mirrors/le/LeaguePrank 厌倦了千篇一律的英雄联盟客户端界面?想向好友展示王者段位却还在白…...
终极指南:用D2DX让《暗黑破坏神2》在现代电脑上焕发新生
终极指南:用D2DX让《暗黑破坏神2》在现代电脑上焕发新生 【免费下载链接】d2dx D2DX is a complete solution to make Diablo II run well on modern PCs, with high fps and better resolutions. 项目地址: https://gitcode.com/gh_mirrors/d2/d2dx 还在为经…...
3步快速部署:智能茅台抢购平台的终极自动化解决方案
3步快速部署:智能茅台抢购平台的终极自动化解决方案 【免费下载链接】campus-imaotai i茅台app自动预约,每日自动预约,支持docker一键部署(本项目不提供成品,使用的是已淘汰的算法) 项目地址: https://gi…...
AICoverGen终极指南:快速创建AI翻唱歌曲的完整教程
AICoverGen终极指南:快速创建AI翻唱歌曲的完整教程 【免费下载链接】AICoverGen A WebUI to create song covers with any RVC v2 trained AI voice from YouTube videos or audio files. 项目地址: https://gitcode.com/gh_mirrors/ai/AICoverGen 想要让你的…...
