Customized View series in personal development (click to View the collection if necessary)

  1. Android custom view first bullet (anti-millet meter step)
  2. Android custom View second bullet (rotating weight)
  3. Android custom View third bullet (anti-human ruler)
  4. Android custom View fourth bullet (Kotlin streaming layout)

It’s been more than a year since the last custom View, but this time I’m going to introduce you to the sliding star rating, although Google does offer it officiallyRatingBarBut I can’t meet my needs, so I have to define one by myself. Without further ado, above:This selection and the default heart shape are images provided by the UI, with the following code:

1. Custom view code

import android.content.Context
import android.graphics.*
import android.graphics.drawable.Drawable
import android.util.AttributeSet
import android.view.MotionEvent
import android.view.View
import android.view.ViewConfiguration
import cn.neoclub.uki.R
import kotlin.math.abs
import kotlin.math.ceil
import kotlin.math.roundToInt

/** * Author: Mr.Dong * Date: 2022/2/15 4:31 PM * Description
class HeartRatingBar : View {
    private var starDistance = 0 // Star spacing
    private var starCount = 5 // Number of stars
    private var starSize = 0 // The height of a star. A star is usually square and its width is equal to its height
    private var starMark = 0 // Score stars
    private var starFillBitmap: Bitmap? = null / / bright stars
    private var starEmptyDrawable : Drawable? = null/ / dark stars
    private var onStarChangeListener : OnStarChangeListener? = null// Listen to the star change interface
            
    private var paint : Paint? = null// Draw the star brush
    // Whether to display integer stars
    private var integerMark = false
    // Initialization can be defined as the distance of the slide (beyond this distance is the slide, otherwise is the click event)
    private var scaledTouchSlop:Int=0

    constructor(context: Context, attrs: AttributeSet?) : super(context, attrs) {
        init(context, attrs)
    }

    constructor(context: Context, attrs: AttributeSet? , defStyleAttr:Int) : super(
        context,
        attrs,
        defStyleAttr
    ) {
        init(context, attrs)
    }

    /** * Initialize the UI component **@param context
     * @param attrs
     */
    private fun init(context: Context, attrs: AttributeSet?). {
        // Get the effective sliding distance
        scaledTouchSlop=ViewConfiguration.get(context).scaledTouchSlop
        isClickable = true
        // Get the values of various attributes
        val mTypedArray = context.obtainStyledAttributes(attrs, R.styleable.HeartRatingBar)
        starDistance = mTypedArray.getDimension(R.styleable.HeartRatingBar_starDistance, 0f).toInt()
        starSize = mTypedArray.getDimension(R.styleable.HeartRatingBar_starSize, 20f).toInt()
        starCount = mTypedArray.getInteger(R.styleable.HeartRatingBar_starCount, 5)
        starEmptyDrawable = mTypedArray.getDrawable(R.styleable.HeartRatingBar_starEmpty)
        starFillBitmap = drawableToBitmap(mTypedArray.getDrawable(R.styleable.HeartRatingBar_starFill))
        mTypedArray.recycle()
        paint = Paint()
        // Set anti-aliasingpaint? .isAntiAlias =true
        // Set the rendererpaint? .shader = BitmapShader(starFillBitmap!! , Shader.TileMode.CLAMP, Shader.TileMode.CLAMP) }/** * Sets whether integer scoring is required *@param integerMark
     */
    fun setIntegerMark(integerMark: Boolean) {
        this.integerMark = integerMark
    }

    /** * Sets the number of stars to display **@param mark
     */
    private fun setStarMark(mark: Int) {
        starMark = if (integerMark) {
            Math.ceil(100.675) = 101.0; // The ceil function returns double after the decimal point
            ceil(mark.toDouble()).toInt()
        } else {
            (mark * 10).toFloat().roundToInt() * 1 / 10
        }
        if(onStarChangeListener ! =null) { onStarChangeListener? .onStarChange(starMark)// Call the listening interface
        }
        invalidate()
    }

    /** * gets the number of stars to display **@return starMark
     */
    fun getStarMark(a): Int {
        return starMark
    }

    /** * defines the listening interface for star clicks */
    interface OnStarChangeListener {
        fun onStarChange(mark: Int)
    }

    /** * set listener *@param onStarChangeListener
     */
    fun setOnStarChangeListener(onStarChangeListener: OnStarChangeListener?). {
        this.onStarChangeListener = onStarChangeListener
    }

    override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
        super.onMeasure(widthMeasureSpec, heightMeasureSpec)
        // Set the width and height of the view to inherit the view must override this method
        setMeasuredDimension(starSize * starCount + starDistance * (starCount - 1), starSize)
    }

    override fun onDraw(canvas: Canvas) {
        super.onDraw(canvas)
        if (starFillBitmap == null || starEmptyDrawable == null) {
            return
        }
        // Draw an empty star
        for (i in 0 until starCount) {
            // Set the rectangle to be drawn by starEmptyDrawable, which can be drawn directly after calling draw()starEmptyDrawable? .setBounds( (starDistance + starSize) * i,0, (starDistance + starSize) * i + starSize, starSize ) starEmptyDrawable? .draw(canvas) }if (starMark > 1) {
            // Draw the first star
            canvas.drawRect(0f.0f, starSize.toFloat(), starSize.toFloat(), paint!!)
            if (starMark - starMark == 0) { // This is the first step
                // Draw bright stars
                for (i in 1 until starMark) {
                    // Width of each shift start + spacing
                    canvas.translate((starDistance + starSize).toFloat(), 0f)
                    canvas.drawRect(0f.0f, starSize.toFloat(), starSize.toFloat(), paint!!) }}else { // Non-shaping star draw goes here
                for (i in 1 until starMark - 1) {
                    canvas.translate((starDistance + starSize).toFloat(), 0f)
                    canvas.drawRect(0f.0f, starSize.toFloat(), starSize.toFloat(), paint!!)
                }
                canvas.translate((starDistance + starSize).toFloat(), 0f)
                canvas.drawRect(
                    0f.0f,
                    starSize * (((starMark - starMark) * 10).toFloat().roundToInt() * 1.0 f / 10), starSize.toFloat(), paint!! ) }}else {
            //startMark=0 draws nothing
            canvas.drawRect(0f.0f, (starSize * starMark).toFloat(), starSize.toFloat(), paint!!) }}// Record the position of the last down x
    private var downX:Int=0

    override fun onTouchEvent(event: MotionEvent): Boolean {
        var x = event.x.toInt()
        if (x < 0) x = 0
        if (x > measuredWidth) x = measuredWidth
        when (event.action) {
            MotionEvent.ACTION_DOWN -> {
                downX=x
                // Restriction on divisor not being 0
                if(starCount==0||(measuredWidth * 1 / starCount)==0) {return false
                }
                val count=x * 1 / (measuredWidth * 1 / starCount)
                setStarMark(count+1)
            }
            MotionEvent.ACTION_MOVE -> {
                // If the absolute value of the sliding distance is less than the officially defined effective sliding distance, move will not be processed as down
                if(abs(event.x-downX)<scaledTouchSlop){
                    return false
                }
                if(starCount==0||(measuredWidth * 1 / starCount)==0) {return false
                }
                setStarMark(x * 1 / (measuredWidth * 1 / starCount))
            }
            MotionEvent.ACTION_UP -> {}
        }
        invalidate()
        return super.onTouchEvent(event)
    }

    /** * drawable to bitmap **@param drawable
     * @return* /
    private fun drawableToBitmap(drawable: Drawable?).: Bitmap? {
        if (drawable == null) return null
        val bitmap = Bitmap.createBitmap(starSize, starSize, Bitmap.Config.ARGB_8888)
        val canvas = Canvas(bitmap)
        drawable.setBounds(0.0, starSize, starSize)
        drawable.draw(canvas)
        return bitmap
    }
}
Copy the code

2. The use of custom View

    <cn.neoclub.uki.message.widget.HeartRatingBar
        android:id="@+id/rb_rating_bar"
        android:layout_width="match_parent"
        android:layout_height="40dp"
        app:starCount="5"
        app:starDistance="7dp"
        app:starEmpty="@drawable/icon_heart_rating_default"
        app:starFill="@drawable/icon_heart_rating_select"
        app:starSize="40dp" />
Copy the code

3. Attributes in the attrs. XML file

 <declare-styleable name="HeartRatingBar">
        <attr name="starDistance" format="dimension"/>
        <attr name="starSize" format="dimension"/>
        <attr name="starCount" format="integer"/>
        <attr name="starEmpty" format="reference"/>
        <attr name="starFill" format="reference"/>
    </declare-styleable>
Copy the code

4. Send you two pictures, in case you can’t run

1.icon_heart_rating_select.png

2.icon_heart_rating_default.png

Do you feel that there is an icon missing? Yes, there is an 😄 missing (actually there is a picture).