preface

As for the custom View, I believe you are already familiar with it. Today, I want to share a little bit about customizing a View, which is customizing Drawable.

Drawable is an abstract class for Drawable objects. Compared to View, it is more pure. It only deals with drawing and does not handle user interaction events, so it is suitable for background drawing.

Before we get into custom drawables, let’s take a look at some common drawables.

Introduction to drawable object resources

Drawable objects are graphics that can be drawn on the screen, which can be obtained by methods such as getDrawable(int) and then applied to property methods such as Android: Drawable and Android: Icon.

Here are some common drawable objects, which I will introduce in three steps:

  1. To introduce you toXML(examples will be given).
  2. Then I introduce the property methods.
  3. And then in the form of code to dynamically implementXML(examples will be given).

BitmapDrawable

Bitmap image. Android supports bitmap files in three formats:.png(preferred),.jpg(acceptable), and.gif(not recommended). We can refer to bitmap files directly using file names as resource ids, or we can create alias resource ids in XML files, which are called XML bitmaps.

XML bitmap: defined by the XML file, pointing to the bitmap file, the file is located in the res/drawable/filename. The XML, the file name is as a reference resource ID, such as: R.d rawable. The filename.



about<bitmap>Properties:

  • android:src: references drawable object resources,necessary.
  • android:tileMode: Defines tiling mode. When tiling mode is enabled, the bitmap repeats,And pay attention to: Once tiling mode is enabled,android:gravityThe attribute will be ignored.

    The value defining the tile property must be one of the following:
    • disabled: Uneven shop map, default value.
    • clamp: Copies the edge color when the shader draws beyond its original bounds.
    • repeat: horizontal and vertical repeat shader image.
    • mirror: The image of the shader is repeated horizontally and vertically, alternating mirrored images so that adjacent images are always connected.

Note: The Android :gravity property is ignored when tile mode is enabled.

  • android:gravity: Defines the gravity property of the bitmap. When the bitmap is smaller than the container, the position of the object in its container can be drawn.
    • top: Puts an object on top of its container without changing its size.
    • bottom: Puts an 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: according to the need,extensionThe vertical size of the object so that it fits perfectly into its container.
    • center_horizontal: Places the object in the horizontal center of its container without changing its size.
    • fill_horizontal: according to the need,extensionThe horizontal size of an object so that it fits perfectly into its container.
    • center: Centers the object on the horizontal and vertical axes of its container without changing its size.
    • fill: Extends the vertical size of the object as needed to fully fit its container. This is the default value.
    • clip_vertical: Additional option that can be set to crop the top and/or bottom edges of child elements to their container boundaries. Clipping is based on vertical gravity: top gravity clipping the upper edge, bottom gravity clipping the lower edge, neither side clipping at the same time.
    • clip_horizontal: Additional option that can be set to have the left and/or right sides of child elements crop to their container boundaries. Clipping is based on horizontal gravity: left gravity clipping the right edge, right gravity clipping the left edge, neither side clipping at the same time.

In addition to defining bitmaps in XML files, we can also do this directly in code, called BitmapDrawable.

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

The effect picture is as follows:


LayerDrawable

LayerDrawable: a drawable composed of a list of drawable objects. Each drawable object in the list is drawn in list order, with the last drawable object in the list drawn at the top.

Each drawable is represented by a

element within a single

element.

Describe the attributes:

  • <layer-list>: mandatory root element. Contains one or more<item>Elements.
  • <item>Is:<layer-list>Element, whose properties allow you to define the position on the layer.
    • android:drawable:necessary. Reference to drawable object resources.
    • android:top: integer. Top offset (pixels).
    • android:right: integer. Right offset (pixels).
    • android:bottom: integer. Bottom offset (pixels).
    • android:left: integer. Left offset (pixels).

In addition to implementing it in XML, we can also achieve the same effect in code.

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
}
valarr = 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
Copy the code

The effect picture is as follows:


StateListDrawable

StateListDrawable: Multiple different images are used to represent the same graph, depending on the state of the object.

android:state_pressed="true" android:state_pressed="false"

Describe the attributes:

  • <selector>:necessaryThe root element of. Contains one or more<item>Elements.
  • <item>: Defines drawable objects to be used during certain states<selector>The child of the element.

Its properties:

    • android:drawable: references drawable object resources,necessary.
    • android:state_pressed: Boolean value. Whether to press an object (e.g. touch/click on a button).
    • android:state_checked: Boolean value. Whether the object is selected.
    • android:state_enabled: Boolean value. Whether you can receive touch or click events.

In addition to implementing it in XML, we can also achieve the same effect in code.

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")}}Copy the code

LevelListDrawable

LevelListDrawable: manages the list of drawable objects. Each drawable object has a Level limit set. When setLevel() is used, drawable resources in the Level list whose android:maxLevel value is greater than or equal to the value passed to the method are loaded.

Describe the attributes:

  • <level-list>: mandatory root element. Contains one or more<item>Elements.
  • <item>: Drawable objects used at a specific level.
    • android:drawable:necessary. Reference to drawable object resources.
    • android:maxLevel: integer. According to theItemThe highest level allowed.
    • android:minLevel: integer. According to theItemThe lowest level allowed.

After the Drawable is applied to the View, the level can be changed through setLevel() or setImageLevel().

In addition to implementing it in XML, we can also achieve the same effect in code.

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(a) {

        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 levellevelListDrawable? .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?).{}})}}}Copy the code

The effect picture is as follows:


TransitionDrawable

TransitionDrawable: staggered fade-out between two drawable resources.

Describe the attributes:

  • <transition>: mandatory root element. Contains one or more<item>Elements.
  • <item>: Convert part of the drawable object.
    • android:drawable:necessary. Reference to drawable object resources.
    • android:top,android:bottom,android:left,android:right: integer. Offset (pixels).

Note: No more than two items are allowed. Call startTransition() for forward conversion and reverseTransition() for backward conversion.

In addition to implementing it in XML, we can also achieve the same effect in code.

class TransitionDrawableFragment : BaseFragment<FragmentTransitionDrawableBinding>() {

    private var isShow = false
    private lateinit var manualDrawable: TransitionDrawable

    override fun initView(a) {
        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(a) {
        if (isShow) {
            manualDrawable.reverseTransition(3000)}else {
            manualDrawable.startTransition(3000)}}override fun onResume(a) {
        super.onResume() setTransition() isShow = ! isShow } }Copy the code

The effect picture is as follows:


InsetDrawable

InsetDrawable: Inserts other drawable objects at a specified distance. This class of drawable objects is useful when a view needs a background smaller than the view’s actual boundary.

To introduce its properties:

  • <inset>:necessary. The root element.
  • android:drawable:necessary. Reference to drawable object resources.
  • android:insetTop,android:insetBottom,android:insetLeft,android:insetRight: it is the right size. Inserted, represented by size

In addition to implementing it in XML, we can also achieve the same effect in code.

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

The effect picture is as follows:


ClipDrawable

ClipDrawable: Clipable objects according to level level. You can control the width and height of subtractable objects according to level and gravity.


      
<clip xmlns:android="http://schemas.android.com/apk/res/android"
    android:drawable="@drawable/nick"
    android:clipOrientation="horizontal"
    android:gravity="center">

</clip>
Copy the code

To introduce its properties:

  • <clip>:necessary. The root element.
  • android:drawable:necessary. Reference to drawable object resources.
  • android:clipOrientation: Cutting direction.
    • horizontal: Horizontal clipping.
    • vertical: Vertical cutting.
  • android:gravity: Gravity attribute.

Finally, the clipping is achieved by setting the level level. The default level of level is 0, that is, the clipping is complete, so that the image is not visible. When the level is 10,000, the image is not clipped but fully visible.

In addition to implementing it in XML, we can also achieve the same effect in code.

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(a) {
        binding.clipDrawableInclude.apply {
            tv1.setText(R.string.clip_drawable)
            tv1.background = clipDrawable
            tv2.setText(R.string.clip_drawable)
            tv2.background = manualClipDrawable
        }

        //level The default level is 0, that is, the image is completely cropped so that the image is not visible. When the level is 10,000, the image is not clipped but fully visible.
        binding.seekBar.apply {
            //init levelclipDrawable? .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?).{}})}}}Copy the code

The effect picture is as follows:


ScaleDrawable

ScaleDrawable: Changes the size of a drawable object based on its level.


      
<scale xmlns:android="http://schemas.android.com/apk/res/android"
    android:drawable="@drawable/nick"
    android:scaleWidth="100%"
    android:scaleHeight="100%"
    android:scaleGravity="center">

</scale>
Copy the code

To introduce its properties:

  • <scale>:necessary. The root element.
  • android:drawable:necessary. Reference to drawable object resources.
  • android:scaleGravity: Specifies the gravity position after scaling.
  • android:scaleHeight: percentage. Scale height, expressed as the percentage of an object’s boundaries that can be drawn. The value is in XX% format. For example: 100%, 12.5%, etc.
  • android:scaleWidth: percentage. The scale width, expressed as the percentage of an object’s borders that can be drawn. The value is in XX% format. For example: 100%, 12.5%, etc.

In addition to implementing it in XML, we can also achieve the same effect in code.

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?).{}})}Copy the code

The effect picture is as follows:


ShapeDrawable

Shape-drawable: Drawable objects of various shapes are defined through XML.

To introduce its properties:

  • <shape>:necessary. The root element.
  • android:shape: Defines the type of the shape.
    • rectangle: The default shape to fill the rectangle containing the view.
    • oval: ADAPTS to elliptical shapes that contain view dimensions.
    • line: spans a horizontal line that contains the width of the view. This shape requires the element to define the line width.
    • ring: ring.
      • android:innerRadius: it is the right size. The radius inside the ring (the hole in the middle).
      • android:thickness: it is the right size. Ring thickness.
  • <corners>: Rounded corner, applicable only when the shape is rectangular.
    • android:radius: it is the right size. The radius of all the angles. If you want to set a single Angle, you can use thisandroid:topLeftRadius,android:topRightRadius,android:bottomLeftRadius,android:bottomRightRadius.
  • <padding>: Sets the inner margin.
    • android:left: it is the right size. Set the left inner margin. As well asandroid:right,android:top,android:bottomChoose from.
  • <size>: Size of a shape.
    • android:height: it is the right size. The height of the shape.
    • android:width: it is the right size. The width of the shape.
  • <solid>: Solid color to fill the shape.
    • android:color: color.
  • <stroke>: Shape strokes
    • android:width: it is the right size. Line width.
    • android:color: color. The color of the line.
    • android:dashGap: it is the right size. The spacing of short lines. Dashed line effect.
    • android:dashWidth: it is the right size. The size of each dash. Dashed line effect.

In addition to implementing it in XML, we can also achieve the same effect in code.

class ShapeDrawableFragment : BaseFragment<FragmentShapeDrawableBinding>() {

    override fun initView(a) {
        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 works better */
    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) } } }Copy the code

The effect picture is as follows:


GradientDrawable

GradientDrawable object: As the name suggests, implements a gradient color effect. It’s also a ShapeDrawable.

To introduce its properties:

  • <shape>:necessary. The root element.
  • gradient: Indicates the gradient color.
    • android:angle: integer. Represents the Angle of the gradient. 0 means left to right, 90 means top to top.Pay attention to: Must be a multiple of 45. The default value is 0.
    • android:centerX: Floating point type. Represents the position of the gradient center relative to the X-axis (0-1.0).android:centerYIn the same way.
    • android:startColor: color. Start color.android:endColor,android:centerColorIndicates the end color and the middle color respectively.
    • android:gradientRadius: Floating point type. Radius of gradient. Only in theandroid:type="radial"When applicable.
    • android:type: Type of gradient.
      • linear: Linear gradient. The default is this type.
      • radial: Radial gradient, also known as radar gradient, with the start color as the center color.
      • sweep: Streamlined gradient.

In addition to implementing it in XML, we can also achieve the same effect in code.

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
Copy the code

The effect picture is as follows:


AnimationDrawable

AnimationDrawable: A drawable object used to create frame-by-frame animation.


      
<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>
Copy the code

To introduce its properties:

  • <animation-list>:necessary. The root element.
  • <item>: Drawable object for each frame.
    • android:drawable:necessary. Reference to drawable object resources.
    • android:duration: Duration of the frame, in milliseconds.
    • android:oneshot: Boolean value. Indicates whether to show the animation only once. The default isfalse.

In addition to implementing it in XML, we can also achieve the same effect in code.

valanimationDrawable = 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()
Copy the code

The effect picture is as follows:


A custom Drawable

Having introduced several common Drawable object resources, let’s take a closer look at how to customize 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(a): Int {
        TODO("Not yet implemented")}}Copy the code

As you can see from the above code, we need to inherit Drawable() and implement four methods:

  • setAlpha: in order toDrawableTo specify aalphaValue, 0 indicates complete transparency and 255 indicates complete opacity.
  • setColorFilter: in order toDrawableSpecifies optional color filters.DrawablethedrawEach output pixel of the drawing content is mixed intoCanvasThe render target will be modified by the color filter before. passnullRemoves any existing color filters.
  • getOpacityReturns theDrawableTransparency, as shown below:
    • PixelFormat.TRANSLUCENT: translucent.
    • PixelFormat.TRANSPARENT: transparent.
    • PixelFormat.OPAQUE: opaque.
    • PixelFormat.UNKNOWN: unknown.
  • draw: Draws within boundaries (throughsetBounds()),alphawithcolorFilterThe impact.

I’ll give you an example.

Example: Rolling a basketball

Function introduction: when we click the screen, the basketball will roll to the coordinate.

As shown below:

The implementation steps can be divided into two simple steps:

  1. Draw a basketball.
  2. Get the user click coordinates and use the property animation to scroll the basketball to that position.

Draw the basketball

The first step is to draw the basketball. This step does not need to interact with the user, so we use a custom Drawable to draw.

As follows:

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(a): Int {
        return when (paint.alpha) {
            0xff -> PixelFormat.OPAQUE
            0x00 -> PixelFormat.TRANSPARENT
            else -> PixelFormat.TRANSLUCENT
        }
    }

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

rolling

After the basketball is drawn, the next step is to get the user’s click coordinates. For a better example, HERE I put it in a custom View to complete.

As follows:

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 eventcanvas? .drawCircle(rawTouchEventX, rawTouchEventY, rippleRadius, ripplePaint) } }Copy the code

To recap: first we will dynamically generate a View with a background set to the BallDrawable() we just drew to form a basketball. Then, the onTouchEvent() method is used to get the user’s click coordinates, and the ball is rolled to this coordinate through the property animation.

For more code, see Github Drawable_Leaning’s basketball rolling.

conclusion

In this article we learned about several common drawables, as well as about custom drawables. We learned that drawables are only used for drawing, not for user interaction events. So, in our complex custom View, we can split it up, and some backgrounds, decorations, and so on can be completely drawn by custom Drawable. This will make our complex custom View layer more clear, code readability greatly improved.

If you want to refer to all of the source code in this article, you can click on Github Drawable_Learning to see it. You are welcome to give me a little star.

Reference Documents:

  • Drawable object resources

In fact, the biggest purpose of sharing articles is to wait for someone to point out my mistakes. If you find any mistakes, please point them out without reservation and consult with an open mind.


In addition, if you think the article is good and helpful to you, please give me a thumbs up, thank you! Every time you like is to say to me: come on! A stranger. This will give me continuous motivation, thanks again! Peace~