Try not to rewrite the Layout method

Here’s an example:

class ErrorView @JvmOverloads constructor( context: Context, attrs: AttributeSet? = null, defStyleAttr: Int = 0 ) : View(context, attrs, defStyleAttr) { override fun layout(l: Int, t: Int, r: Int, b: Int) {super.layout(l, t, r + 100, b + 100)}}Copy the code

So this custom view, let’s put it inside the LinearLayout

<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="horizontal">

    <com.example.testmoveviewpager.ErrorView
        android:layout_width="100dp"
        android:background="@color/teal_200"
        android:layout_height="100dp">

    </com.example.testmoveviewpager.ErrorView>

    <com.example.testmoveviewpager.ErrorView
        android:layout_width="100dp"
        android:background="@color/purple_700"
        android:layout_height="100dp">

    </com.example.testmoveviewpager.ErrorView>

</LinearLayout>

Copy the code

The layout file should have two views side by side with the same width. Of course, this width and height should be 100 px more than the width and height configured in layout_width. After all, we set the width and height of layout_width in layout. Now look at the results:

You can obviously see that the effect is quite different from what we expected. Here’s why: The parent view is also called linearLayout. The layout of the viewgroup is made according to the result of the measure. We directly modify the layout of the child view without modifying the logic of the measure. As a result, the parent view or according to the result of the measure to layout naturally out of the result is very inconsistent with the expected. In fact:

Layout and measure can modify the final width and height of the view, but in 99% of cases, we do not need to modify the layout method when we customize the view. Once this method is modified, it will affect the work of the parent view and the result is hard to predict. We just change the logic inside the onMeasure method 99 percent of the time

Don’t forget to handle wrAP_content

Look at the code below

class CircleView2 @JvmOverloads constructor(
    context: Context, attrs: AttributeSet? = null, defStyleAttr: Int = 0
) : androidx.appcompat.widget.AppCompatImageView(context, attrs, defStyleAttr) {

    val paint = Paint().apply {
        color = Color.RED
    }

    val radius = 100f

    override fun onDraw(canvas: Canvas) {
        canvas.drawCircle(
            (width / 2).toFloat(),
            (height / 2).toFloat(),
            radius,
            paint
        )
    }

    
}
Copy the code

You draw a circle, but you don’t override the onMeasure and it actually doesn’t show up on the screen once the property is wrAP_content (of course you can still show it if you write it as a fixed dp)

So make the following changes:

class CircleView2 @JvmOverloads constructor(
    context: Context, attrs: AttributeSet? = null, defStyleAttr: Int = 0
) : androidx.appcompat.widget.AppCompatImageView(context, attrs, defStyleAttr) {

    val paint = Paint().apply {
        color = Color.RED
    }

    val radius = 100f

    override fun onDraw(canvas: Canvas) {
        canvas.drawCircle(
            (width / 2).toFloat(),
            (height / 2).toFloat(),
            radius,
            paint
        )
    }


    override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
        var width = radius * 2
        var height = radius * 2

        width = resolveSize(width.toInt(), widthMeasureSpec).toFloat()
        height= resolveSize(height.toInt(), heightMeasureSpec).toFloat()

        setMeasuredDimension(width.toInt(),height.toInt())
    }

}

Copy the code

In fact, there is a fixed routine for custom view measurement, which is to first determine the width and height you want, and then use the resolveSize method to re-measure your width and height. This part is mainly to communicate with the parent view how big you can be, according to the parent view. Finally, call setMeasuredDimension to set the final width

Don’t leave out the padding

A lot of people write custom views that don’t work well, but the most important thing is that they don’t have the padding, and the padding calculation is the key to whether or not your view works well. Again, let’s say that we set

 <com.example.testmoveviewpager.CircleView2
        android:layout_width="wrap_content"
        android:paddingLeft="10dp"
        android:background="@color/teal_200"
        android:layout_height="wrap_content">

    </com.example.testmoveviewpager.CircleView2>
Copy the code

The end result:

You can see that paddingLeft doesn’t work at all, so actually the hardest part of customizing the measurement part of the view is getting the padding right.

For the sake of simplicity, let’s just deal with the paddingLeft case.

class CircleView @JvmOverloads constructor( context: Context, attrs: AttributeSet? = null, defStyleAttr: Int = 0 ) : androidx.appcompat.widget.AppCompatImageView(context, attrs, defStyleAttr) { val paint = Paint().apply { color = Color.RED } val radius = 100f override fun onDraw(canvas: DrawCircle ((width / 2).tofloat () + paddingLeft / 2, (height / 2).tofloat (), radius, paint ) } override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int widthSize = paddingLeft + radius * 2 val width = resolveSize(widthSize.toInt(), widthMeasureSpec) val height = resolveSize((radius * 2).toInt(), heightMeasureSpec) setMeasuredDimension(width, height) } }Copy the code