Welcome to take a look at my open source audio and video library, HardwareVideoCodec is an efficient Android audio and video coding library, supporting soft and hard editing. You can easily encode video at any resolution, regardless of camera preview size. It’s all so simple. At present, we have iterated several stable versions, welcome to study and use, if there are bugs or suggestions, welcome to Issue.

Recently, I have been doing audio and video work, and I have not written the application layer for more than half a year. I am afraid that I am getting rusty. Just recently, I took over an outsourcing project, so I can review it. There is a control in the project is very simple and beautiful, but also the use of technology is more basic, more suitable for beginners to learn, so separate open source, I hope to help beginners.

A common method to customize View

When it comes to customizing a View, you can’t get around the following methods

1. override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int)

When the View is initialized, it is used to measure the size and control the size of the View, such as the width and height ratio of the View.

2. override fun onDraw(canvas: Canvas)

The View’s draw callback, where all the brush and canvas operations are done. Do not perform time-consuming operations in this method. Everything that can be evaluated externally is evaluated externally, and try not to initialize variables here. Because this method normally does callbacks at 60fps, it will stall if there are time-consuming operations and consume a lot of memory if you initialize a lot of objects. Anyway, anything that doesn’t have anything to do with the canvas, don’t do it here.

3. invalidate()

This is used to tell the View to redraw, which means to call onDraw again, so instead of calling onDraw, which is very common, we can call onDraw when our interface properties change.

4. override fun onTouchEvent(event: MotionEvent): Boolean

As I’m sure you all know, this is the touch event callback. Here you can handle some gestures.

2. Customize a scale control RulerView

Because the code is more, and the source code inside the notes are more detailed, so here just pick a few key methods to explain. If you have any questions or errors, feel free to leave them in the comments section. Looking at the video at the beginning of this article, we can see that while the control looks neat, there are quite a few things to control, including three types of lithography and some text.

  1. Normal scale, short width, light color, no text.
  2. The whole 10 scale, the width is longer, the color is darker than the ordinary scale, and with text.
  3. Vernier scale, the widest of the three scales, highlighted in color, and also with text.
  4. Label text that describes the purpose of the scale.

So we define the following brushes. In order to avoid changing brush properties frequently in onDraw, we also define separate brushes for text and scale. The purpose is to avoid changing any brush properties and changing properties in onDraw, which leads to time-consuming drawing. More importantly, changing the properties of the brush back and forth is too complicated for easy operation and troubleshooting.

  1. ScalePaint: Paint // Scale brushes
  2. ScalePointerPaint: Paint // full 10 scale text brush
  3. ScalePointerTextPaint: Paint // full 10 scale text brush
  4. CursorPaint: Paint // cursor brushes
  5. CursorTextPaint: Paint // cursor text brushes
  6. CursorLabelPaint: Paint // Label text brushes

Initialize parameters from properties set in XML

In addition to the basic brush object, we also need some necessary brush properties, such as when we draw a scale, we need to know the scale position, size, and spacing. So around that, we define a bunch of properties. These attributes can be provided by XML definition, which leads to another important use of View. This usage is relatively fixed, is the same routine. App_scaleWidth is defined in values/attrs.xml. App represents the namespace and can be customized. ScaleWidth is the attribute ID, the same as layout_width. Once we define an attribute ID in a namespace, we can pass attributes from THE XML to the View just as we did with layout_width and layout_height. At this point, you can get these property values directly in the View constructor, as shown below.

/** * Private fun resolveAttribute(context: context, attrs: AttributeSet? , defStyleAttr: Int, defStyleRes: Int) { scaleStrokeWidth = dpToPx(context, 1F) / 2f scaleWidth = 50 scalePointerWidth = (scaleWidth * 1.5).toint () cursorWidth = (scaleWidth * 3.333).toint () scaleHeight = 5 cursorColor = context.resources.getColor(R.color.red) scaleColor = context.resources.getColor(R.color.grey_888) scalePointerColor = context.resources.getColor(R.color.grey_800) val a = context.theme.obtainStyledAttributes(attrs, R.styleable.app, defStyleAttr, defStyleRes)for (i in 0 until a.indexCount) {
        val attr = a.getIndex(i)
        if(attr == R.styleable.app_scaleWidth) { scaleWidth = a.getDimensionPixelOffset(attr, 50) scalePointerWidth = (scaleWidth * 1.5).toint () cursorWidth = (scaleWidth * 3.333).toint ()}else if (attr == R.styleable.app_scaleHeight) {
            scaleHeight = a.getDimensionPixelOffset(attr, 5)
        } else if (attr == R.styleable.app_cursorColor) {
            cursorColor = a.getColor(attr, context.resources.getColor(R.color.red))
        } else if (attr == R.styleable.app_scaleColor) {
            scaleColor = a.getColor(attr, context.resources.getColor(R.color.grey_888))
        } else if (attr == R.styleable.app_scalePointerColor) {
            scalePointerColor = a.getColor(attr, context.resources.getColor(R.color.grey_800))
        }
    }
    cursorTextOffsetLeft = dpToPx(context, 32f)
    a.recycle()
}
Copy the code

2. Draw the View

Instead of using scrollTo and scrollBy provided by the View to control scrolling, this article redefines an X and Y property to record the scrolling position, and uses this property to draw the corresponding position to achieve scrolling effect. This can solve performance problems by specifying the area to draw (off-screen content is not drawn, but interested students can try to do so). DrawScale draws each element by walking through items, including the scale and corresponding text, which is a fairly basic operation. Note that canvas.drawText is x by default,y refers to the lower left corner of the text.

private fun drawScale(canvas: Canvas) {
    for (i in0 until items.size) {// Draw scale according to the given item information val top = offsetHeight + McUrrentorigin. y.int () + I * scaleHeight + scaleHeight / 2,ifDrawRect (RectF(pointerScaleleft.tofloat (), top-ScalestrokeWidth, pointerScaleLeft.toFloat() + scalePointerWidth, top + scaleStrokeWidth), scalePointerPaint)ifMath.abs(getSelectedItem() -i) > 1) {math.abs (getSelectedItem() -i) > 1) Val text = items[I].toString() val size = measureTextSize(scalePointerTextPaint, text) Cancanvas. DrawText (text, PointerScaleLeft - size[0] * 1.3f, top + size[1] / 2, scalePointerTextPaint)}}else{// Draw canvas. DrawRect (RectF(scaleleft.tofloat (), top-ScalestrokeWidth, scaleleft.tofloat () + scaleWidth, Top + scaleStrokeWidth), scalePaint)}}} private fun drawCursor(canvas: Canvas) { val left = scaleLeft + scaleWidth - cursorWidth val top = measuredHeight / 2f canvas.drawRect(RectF(left.toFloat(), top.toInt() - scaleStrokeWidth, left.toFloat() + cursorWidth, top.toInt() + scaleStrokeWidth), cursorPaint) val text = items[getSelectedItem()].toString() val textSize = measureTextSize(cursorTextPaint, text) val labelSize = measureTextSize(cursorLabelPaint, label) val labelLeft = left - cursorTextOffsetLeft - labelSize[0] val textOffset = (textSize[0] - labelSize[0]) / 2f canvas.drawText(text, left - cursorTextOffsetLeft - textSize[0] + textOffset, top + textSize[1] / 2, cursorTextPaint) canvas.drawText(label, labelLeft, top + textSize[1] + labelSize[1], cursorLabelPaint) }Copy the code

3. Support scrolling

Android scroll gesture operation is relatively simple, do not need to achieve a variety of logic control, but through the system provided by the Scroller to calculate the scrolling position. First we need a GestureDetectorCompat for gesture listening and an OverScroller for calculating scroll position via MotionEvent.

1.mGestureDetector: GestureDetectorCompat 2. scroller: OverScroller

private fun init() {
    mGestureDetector = GestureDetectorCompat(context, onGestureListener)
    scroller = OverScroller(context)
}
Copy the code

To construct a GestureDetectorCompat object, you need to provide an OnGestureListener to listen for onScroll and onFling events. The MotionEvent, after being processed by GestureDetectorCompat, becomes a scroll and inertial scroll event that can be used directly, and then notified us via those two callbacks. In onScroll, we calculate the scrolling direction by the horizontal and vertical scrolling distance. If the horizontal scrolling distance is greater than the vertical scrolling distance, we can consider it as horizontal scrolling, and vice versa. This article only needs to scroll vertically. Once we have the scroll direction, we can add up the scroll positions x and y, recording the new position after each slide. Finally inform redrawn by postInvalidateOnAnimation or invalidate, ontouch according to the new x, y, draw corresponding position, to achieve the slide. Although onScroll has realized the View sliding, but only to follow the finger movement, has not realized the “throw” action. In the real world, motion is inertial, and if you just implement onScroll, everything will look very stiff. So how do we do inertial motion, we do it ourselves? Think of all terrible, so many motion functions, I believe that not ordinary people can deal with. Fortunately, this calculation can be handed to GestureDetectorCompat’s onFling. OnFling has four parameters. The first two parameters are motionEvents, which represent the first and second touch events. VelocityX: Float represents the X-axis scrolling speed, and velocityY: Float represents the Y-axis scrolling speed. Now, some of you might be asking, how does a View keep swiping when you leave the screen with your finger and no longer have an event? If you look at the onFling callback, the onFling callback is true. The onFling callback is only called once and does not scroll continuously. So how do we achieve continuous inertial roll? To achieve continuous inertial scrolling, you rely on override Fun computeScroll(), which is called in the DRAW procedure, We can control inertial rolling by invalidate->onDraw->computeScroll->invalidate such a cycle until inertial rolling stops, specific implementation can refer to the source code at the end of the article.

/** * Private val onGestureListener = object: GestureDetector.SimpleOnGestureListenerOverride fun onDown(e: MotionEvent) {override fun onDown(e: MotionEvent): Boolean { parent.requestDisallowInterceptTouchEvent(true)
        mCurrentScrollDirection = Direction.NONE
        return true} / / override fun onScroll(e1: MotionEvent, e2: MotionEvent, distanceX: Float, distanceY: Float): Boolean {// Stop scrolling. forceFinished(true)
//            Log.e(RulerView::class.java, "onScroll: ${mCurrentOrigin.y}.$distanceY")
        if(direction.none == mCurrentScrollDirection) {// Check the scrolling Direction, there is only one vertical Direction mCurrentScrollDirection =if (Math.abs(distanceX) < Math.abs(distanceY)) {
                Direction.VERTICAL
            } else{ Direction.NONE } } // Calculate the new origin after scroll. when (mCurrentScrollDirection) { Direction.VERTICAL -> {// Calculate finger drag distance, And record the new coordinates redraw interface mCurrentOrigin. Y - = distanceY checkOriginY () ViewCompat. PostInvalidateOnAnimation (this @ RulerView)}}return true} / / Override Fun onFling(e1: MotionEvent, e2: MotionEvent, velocityX: Float, velocityY: Float): Boolean { scroller.forceFinished(true)
        mCurrentFlingDirection = mCurrentScrollDirection
        when (mCurrentFlingDirection) {
            Direction.VERTICAL -> scroller.fling(mCurrentOrigin.x.toInt(), mCurrentOrigin.y.toInt(),
                    0, velocityY.toInt(), Integer.MIN_VALUE,
                    Integer.MAX_VALUE, Integer.MIN_VALUE, 0)
        }
        ViewCompat.postInvalidateOnAnimation(this@RulerView)
        return true}}Copy the code

So we’re done with the two important parts of custom View drawing and events. If you like, please remember to like, comment and follow, your attention is my encouragement. The article finally posted the relevant source code, welcome to study. If you have any questions or errors, feel free to leave them in the comments section.

The complete code

RulerView. Kt

class RulerView : View {
    private enum class Direction {
        NONE, VERTICAL
    }

    private var label: String = "LABEL"private var items: List<*> = ItemCreator.range(0, 60) private var cursorColor = 0 private var scaleColor = 0 private var scalePointerColor = 0 Private var scrollHeight = 0f // scaleWidth private var scaleWidth = 0 // scaleWidth of 10 private var scalePointerWidth = 0 // cursorWidth private var cursorWidth = 0 private var scaleHeight = 0 private var scaleStrokeWidth = 0f Private lateinit var scalePaint: Paint Paint: private lateinit var scalePointerTextPaint: private lateinit var cursorPaint: Paint // Cursor text brush private lateinit var cursorTextPaint: Paint Private var cursorTextOffsetLeft = 0 private var cursorTextOffsetLeft = 0 ScaleLeft = 0 private var pointerScaleLeft = 0 private var scroller: OverScroller private var maxFlingVelocity = 0 private var minFlingVelocity = 0 private var touchSlop = 0 Private var mCurrentFlingDirection = direction.none private var mCurrentFlingDirection = direction.none Private val mCurrentOrigin = PointF(0f, 0f) private lateInit var mGestureDetector: GestureDetectorCompat constructor(context: Context) : super(context) { resolveAttribute(context, null, 0, 0) init() } constructor(context: Context, attrs: AttributeSet?) : super(context, attrs) { resolveAttribute(context, attrs, 0, 0) init() } constructor(context: Context, attrs: AttributeSet? , @AttrRes defStyleAttr: Int) : super(context, attrs, defStyleAttr) { resolveAttribute(context, attrs, defStyleAttr, 0) init()} /** * Initialize parameters from XML attributes */ private fun resolveAttribute(context: context, attrs: AttributeSet? , defStyleAttr: Int, defStyleRes: Int) { scaleStrokeWidth = dpToPx(context, 1F) / 2f scaleWidth = 50 scalePointerWidth = (scaleWidth * 1.5).toint () cursorWidth = (scaleWidth * 3.333).toint () scaleHeight = 5 cursorColor = context.resources.getColor(R.color.red) scaleColor = context.resources.getColor(R.color.grey_888) scalePointerColor = context.resources.getColor(R.color.grey_800) val a = context.theme.obtainStyledAttributes(attrs, R.styleable.app, defStyleAttr, defStyleRes)for (i in 0 until a.indexCount) {
            val attr = a.getIndex(i)
            if(attr == R.styleable.app_scaleWidth) { scaleWidth = a.getDimensionPixelOffset(attr, 50) scalePointerWidth = (scaleWidth * 1.5).toint () cursorWidth = (scaleWidth * 3.333).toint ()}else if (attr == R.styleable.app_scaleHeight) {
                scaleHeight = a.getDimensionPixelOffset(attr, 5)
            } else if (attr == R.styleable.app_cursorColor) {
                cursorColor = a.getColor(attr, context.resources.getColor(R.color.red))
            } else if (attr == R.styleable.app_scaleColor) {
                scaleColor = a.getColor(attr, context.resources.getColor(R.color.grey_888))
            } else if(attr == R.styleable.app_scalePointerColor) { scalePointerColor = a.getColor(attr, context.resources.getColor(R.color.grey_800)) } } cursorTextOffsetLeft = dpToPx(context, 32f) a.ricycle ()} /** * Initializes brushes, scroll controllers and gesture objects */ private funinit() { scroller = OverScroller(context) mGestureDetector = GestureDetectorCompat(context, onGestureListener) maxFlingVelocity = ViewConfiguration.get(context).scaledMaximumFlingVelocity minFlingVelocity = ViewConfiguration.get(context).scaledMinimumFlingVelocity touchSlop = ViewConfiguration.get(context).scaledTouchSlop scalePaint = Paint(Paint.ANTI_ALIAS_FLAG) scalePaint.color = scaleColor scalePaint.style = Paint.Style.FILL scalePointerPaint = Paint(Paint.ANTI_ALIAS_FLAG) scalePointerPaint.color = scalePointerColor scalePointerPaint.style = Paint.Style.FILL scalePointerTextPaint = Paint(Paint.ANTI_ALIAS_FLAG) scalePointerTextPaint.color = scaleColor scalePointerTextPaint.style = Paint.Style.FILL scalePointerTextPaint.textSize = spToPx(context, 14f).toFloat() cursorPaint = Paint(Paint.ANTI_ALIAS_FLAG) cursorPaint.color = cursorColor cursorPaint.style = Paint.Style.FILL cursorTextPaint = Paint(Paint.ANTI_ALIAS_FLAG) cursorTextPaint.color = context.resources.getColor(R.color.black_232) cursorTextPaint.style = Paint.Style.FILL cursorTextPaint.textSize = spToPx(context, 32f).toFloat() cursorLabelPaint = Paint(Paint.ANTI_ALIAS_FLAG) cursorLabelPaint.color = scalePointerColor cursorLabelPaint.style = Paint.Style.FILL cursorLabelPaint.textSize = spToPx(context, 16f).tofloat ()} /** * Set item data */ funsetItems(items: List<*>) { this.items = items this.scrollHeight = (height + (this.items.size - 1) * scaleHeight).toFloat() post { McUrrentorigin.x = 0f McUrrentorigin.y = 0f invalidate()}} /** * get item data */ getItems(): List<*> {returnItems} /** * Sets the label text */ funsetLabel(label: String) {this.label = label // reinitializes the scale left distance initScaleLeft() // Notification to redraw invalidate()} /** * Touch events to mGestureDetector */ override fun onTouchEvent(event: MotionEvent): Boolean {val result = mGestureDetector. OnTouchEvent (event) / / if the finger away from the screen, and there is no inertia slidingif (event.action == MotionEvent.ACTION_UP && mCurrentFlingDirection == Direction.NONE) {
            if(mCurrentScrollDirection == direction.vertical) {snapScroll()} mCurrentScrollDirection = direction.none}returnResult} /** * calculate how the View slides */ override funcomputeScroll() {
        super.computeScroll()
        if(scroll.isfinished) {// Roll and finishif(mCurrentFlingDirection ! == Direction.NONE) { // Snap to day after fling is finished. mCurrentFlingDirection = Direction.NONE SnapScroll ()// Check to see if the scale needs to be aligned, and if so, automatically scroll to align the cursor with the scale}}else{// If you are not currently scrolling, check again to see if you need to align the scalesif(mCurrentFlingDirection ! = Direction.NONE && forceFinishScroll()) { snapScroll() }else if(scroller.com puteScrollOffset ()) {/ / check whether rolling is completed, Y = scroll.curry.tofloat ()// Records the current y coordinate checkOriginY()// checks if the coordinate is out of bounds ViewCompat. PostInvalidateOnAnimation (this) / / notify redraw}else{// do not scroll val startY =if (mCurrentOrigin.y > 0)
                    0f
                else if (mCurrentOrigin.y < height - measuredHeight)
                    measuredHeight - scrollHeight
                elseMcUrrentorigin.y scroll. startScroll(0, starty.toint (), 0, 0, 0)}}} /** * Override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) Int) { super.onMeasure(widthMeasureSpec, heightMeasureSpec) val width = measuredWidth val height = measuredHeight offsetHeight = height / 2 - scaleHeight / 2 scrollHeight = height + (items.size - 1f) * scaleHeight initScaleLeft() pointerScaleLeft = scaleLeft + scaleWidth - ScalePointerWidth} /** * Initializes scale left spacing */ private funinitScaleLeft() {
        val labelSize = measureTextSize(cursorLabelPaint, label)
        scaleLeft = (measuredWidth - scalePointerWidth + cursorTextOffsetLeft + labelSize[0].toInt()) / 2
    }

    override fun onDraw(canvas: Canvas) {
        super.onDraw(canvas)
        if (items.isEmpty()) returnDrawScale (canvas) private fun drawScale(canvas: canvas) {for (i in0 until items.size) {// Draw scale according to the given item information val top = offsetHeight + McUrrentorigin. y.int () + I * scaleHeight + scaleHeight / 2,ifDrawRect (RectF(pointerScaleleft.tofloat (), top-ScalestrokeWidth, pointerScaleLeft.toFloat() + scalePointerWidth, top + scaleStrokeWidth), scalePointerPaint)ifMath.abs(getSelectedItem() -i) > 1) {math.abs (getSelectedItem() -i) > 1) Val text = items[I].toString() val size = measureTextSize(scalePointerTextPaint, text) Cancanvas. DrawText (text, PointerScaleLeft - size[0] * 1.3f, top + size[1] / 2, scalePointerTextPaint)}}else{// Draw canvas. DrawRect (RectF(scaleleft.tofloat (), top-ScalestrokeWidth, scaleleft.tofloat () + scaleWidth, Top + scaleStrokeWidth), scalePaint)}}} private fun drawCursor(canvas: Canvas) { val left = scaleLeft + scaleWidth - cursorWidth val top = measuredHeight / 2f canvas.drawRect(RectF(left.toFloat(), top.toInt() - scaleStrokeWidth, left.toFloat() + cursorWidth, top.toInt() + scaleStrokeWidth), cursorPaint) val text = items[getSelectedItem()].toString() val textSize = measureTextSize(cursorTextPaint, text) val labelSize = measureTextSize(cursorLabelPaint, label) val labelLeft = left - cursorTextOffsetLeft - labelSize[0] val textOffset = (textSize[0] - labelSize[0]) / 2f canvas.drawText(text, left - cursorTextOffsetLeft - textSize[0] + textOffset, top + textSize[1] / 2, cursorTextPaint) canvas.drawText(label, labelLeft, top + textSize[1] + labelSize[1], cursorLabelPaint) } private fun forceFinishScroll(): Boolean {returnScroll. currVelocity <= minFlingVelocity} /** * align with calibration */ private funsnapScroll() { scroller.computeScrollOffset() val nearestOrigin = -getSelectedItem() * scaleHeight mCurrentOrigin.y = NearestOrigin. ToFloat () ViewCompat. PostInvalidateOnAnimation (this @ RulerView)} / check y cross-border * * * * / private funcheckOriginY() {
        if (mCurrentOrigin.y > 0) mCurrentOrigin.y = 0f
        if(McUrrentorigin. y < measuredheight-scrollheight) McUrrentorigin. y = Measuredheight-scrollheight} /** * Gets the selected item */  fun getSelectedItem(): Int { var index = -Math.round(mCurrentOrigin.y / scaleHeight)if (index >= items.size) index = items.size - 1
        if (index < 0) index = 0
        returnIndex} /** * set select item */ funsetSelectedItem(index: Int) { post { mCurrentOrigin.y = -(scaleHeight * index).toFloat() checkOriginY() ViewCompat. PostInvalidateOnAnimation (this @ RulerView) snapScroll ()}} / gestures to monitor * * * * / private val onGestureListener = object : GestureDetector.SimpleOnGestureListenerOverride fun onDown(e: MotionEvent) {override fun onDown(e: MotionEvent): Boolean { parent.requestDisallowInterceptTouchEvent(true)
            mCurrentScrollDirection = Direction.NONE
            return true} / / override fun onScroll(e1: MotionEvent, e2: MotionEvent, distanceX: Float, distanceY: Float): Boolean {// Stop scrolling. forceFinished(true)
//            Log.e(RulerView::class.java, "onScroll: ${mCurrentOrigin.y}.$distanceY")
            if(direction.none == mCurrentScrollDirection) {// Check the scrolling Direction, there is only one vertical Direction mCurrentScrollDirection =if (Math.abs(distanceX) < Math.abs(distanceY)) {
                    Direction.VERTICAL
                } else{ Direction.NONE } } // Calculate the new origin after scroll. when (mCurrentScrollDirection) { Direction.VERTICAL -> {// Calculate finger drag distance, And record the new coordinates redraw interface mCurrentOrigin. Y - = distanceY checkOriginY () ViewCompat. PostInvalidateOnAnimation (this @ RulerView)}}return true} / / Override Fun onFling(e1: MotionEvent, e2: MotionEvent, velocityX: Float, velocityY: Float): Boolean { scroller.forceFinished(true)
            mCurrentFlingDirection = mCurrentScrollDirection
            when (mCurrentFlingDirection) {
                Direction.VERTICAL -> scroller.fling(mCurrentOrigin.x.toInt(), mCurrentOrigin.y.toInt(),
                        0, velocityY.toInt(), Integer.MIN_VALUE,
                        Integer.MAX_VALUE, Integer.MIN_VALUE, 0)
            }
            ViewCompat.postInvalidateOnAnimation(this@RulerView)
            return true} } class ItemCreator { companion object { fun range(start: Int, end: Int): List<*> { val result = ArrayList<Int>() (start.. end).forEach { result.add(it) }return result
            }
        }
    }
    companion object {
        fun dpToPx(context: Context, dp: Float): Int {
            return Math.round(context.resources.displayMetrics.density * dp)
        }

        fun spToPx(context: Context, sp: Float): Int {
            return(TypedValue.applyDimension(2, sp, Context. Resources. DisplayMetrics) + 0.5 f), toInt ()} / measure word wide high * * * * / fun measureTextSize (paint, paint, text: String) : FloatArray {if (TextUtils.isEmpty(text)) return floatArrayOf(0f, 0f)
            val width = paint.measureText(text, 0, text.length)
            val bounds = Rect()
            paint.getTextBounds(text, 0, text.length, bounds)
            return floatArrayOf(width, bounds.height().toFloat())
        }
    }
}
Copy the code

XML attrs.

<? xml version="1.0" encoding="utf-8"? > <resources> <declare-styleable name="app">
        <attr name="scaleWidth" format="dimension" />
        <attr name="scaleHeight" format="dimension" />
        <attr name="cursorColor" format="color" />
        <attr name="scaleColor" format="color" />
        <attr name="scalePointerColor" format="color" />
    </declare-styleable>
</resources>
Copy the code

The sample

<com.lava.demo.widget.RulerView
    android:id="@+id/timeRuler"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:background="#FAFAFA"
    app:cursorColor="#F48B2E"
    app:scaleColor="#C9C9C9"
    app:scaleHeight="8dp"
    app:scalePointerColor="# 999999"
    app:scaleWidth="12dp" />
Copy the code