Article title want to for a long time for a long time, this name is “reading the original code length an implementation of the” knowledge | red dots, but the struggle, feel or should belong to the custom control series ~ ~

The previous chapter introduced two schemes to achieve the little red dot, which are multi-control overlay and single-control rendering. The second scheme has a disadvantage: type binding. It cannot be reused by different types of controls. This article from the parent control point of view, proposed a new solution: container control drawing, to break the type binding.

This is the sixth in a series of tutorials on custom controls.

  1. Android custom controls | View drawing principle (pictures?)
  2. Android custom controls | View drawing principle (pictures?)
  3. Android custom controls | View drawing principle (drawing?)
  4. Android custom controls | source there is treasure in the automatic line feed control
  5. Android custom controls | three implementation of little red dot (on)
  6. Android custom controls | three implementation of little red dot (below)
  7. Android custom controls | three implementation of little red dot (end)

This article was written by Kotlin, and a series of tutorials can be found here

primers

Consider a scenario where there are three different types of controls in a container control that need to display a small red dot in the upper right corner. If you use the “Single control drawing scheme” in the previous article, you must customize three different types of controls to draw a small red dot in the upper right corner of their rectangular region.

Is it possible to hand over the painting to the container control?

The container control can easily know the coordinates of its child control’s rectangle area. What is the way to tell the container control which children need to draw the red dot so that it can draw it in the appropriate position?

readingandroidx.constraintlayout.helper.widget.LayerSource code, found a clever way to tell child control information to the container control.

The inspiration of Layer

Bind associated controls

Layer is a control that works with ConstraintLayout to achieve the following:

That is, to set the background for a set of child controls without increasing the layout level, as follows:


      
<androidx.constraintlayout.widget.ConstraintLayout 
    xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    android:layout_width="match_parent"
    android:layout_height="match_parent">

    <Button
        android:id="@+id/btn3"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="btn3"
        app:layout_constraintEnd_toStartOf="@id/btn4"
        app:layout_constraintHorizontal_chainStyle="spread"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toTopOf="parent" />

    <Button
        android:id="@+id/btn4"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="btn4"
        app:layout_constraintEnd_toStartOf="@id/btn5"
        app:layout_constraintHorizontal_chainStyle="spread"
        app:layout_constraintStart_toEndOf="@id/btn3"
        app:layout_constraintTop_toTopOf="parent" />

    <Button
        android:id="@+id/btn5"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="btn5"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintHorizontal_chainStyle="spread"
        app:layout_constraintStart_toEndOf="@id/btn4"
        app:layout_constraintTop_toTopOf="parent" />//' Add background for 3 buttons'<androidx.constraintlayout.helper.widget.Layer
        android:id="@+id/layer"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:background="#0000ff"/ / 'association3abutton'
        app:constraint_referenced_ids="btn3,btn4,btn5"
        app:layout_constraintEnd_toEndOf="@id/btn5"
        app:layout_constraintTop_toTopOf="@id/btn3"
        app:layout_constraintBottom_toBottomOf="@id/btn3"
        app:layout_constraintStart_toStartOf="@id/btn3"/>
</androidx.constraintlayout.widget.ConstraintLayout>
Copy the code

Add background to Layer and Button associations using app:constraint_referenced_ids=”btn3, bTN4, bTN5 “.

public class Layer extends ConstraintHelper {}

public abstract class ConstraintHelper extends View {}
Copy the code

Layer is a subclass of ConstraintHelper, which is a custom View. So it can be declared as a child control of ConstraintLayout in XML.

ConstraintLayout presumably stores ConstraintHelper as it iterates through the child control. Search ConstraintHelper in ConstraintLayout, and this happens:

public class ConstraintLayout extends ViewGroup {
    //' Store a list of ConstraintHelper '
    private ArrayList<ConstraintHelper> mConstraintHelpers = new ArrayList(4);
    
    //' This method is called when a child control is added to the container '
    public void onViewAdded(View view) {...//' Store a child control of type ConstraintHelper '
        if (view instanceof ConstraintHelper) {
            ConstraintHelper helper = (ConstraintHelper)view;
            helper.validateParams();
            ConstraintLayout.LayoutParams layoutParams = (ConstraintLayout.LayoutParams)view.getLayoutParams();
            layoutParams.isHelper = true;
            if (!this.mConstraintHelpers.contains(helper)) {
                this.mConstraintHelpers.add(helper); }}... }}Copy the code

There should be a method corresponding to onViewAdded() :

public class ConstraintLayout extends ViewGroup {
    //' This method is called when the child control is removed to the container '
    public void onViewRemoved(View view) {...this.mChildrenByIds.remove(view.getId());
        ConstraintWidget widget = this.getViewWidget(view);
        this.mLayoutWidget.remove(widget);
        //' Remove the ConstraintHelper child control '
        this.mConstraintHelpers.remove(view);
        this.mVariableDimensionsWidgets.remove(widget);
        this.mDirtyHierarchy = true; }}Copy the code

ConstraintLayout and ConstraintHelper are not many other things:

public class ConstraintLayout extends ViewGroup {
    private void setChildrenConstraints(a) {... helperCount =this.mConstraintHelpers.size();
        int i;
        if (helperCount > 0) {
            for(i = 0; i < helperCount; ++i) {
                ConstraintHelper helper = (ConstraintHelper)this.mConstraintHelpers.get(i);
                //' Update before going through all ConstraintHelper notification layouts'
                helper.updatePreLayout(this); }}... }protected void onLayout(boolean changed, int left, int top, int right, int bottom) {... helperCount =this.mConstraintHelpers.size();
        if (helperCount > 0) {
            for(int i = 0; i < helperCount; ++i) {
                ConstraintHelper helper = (ConstraintHelper)this.mConstraintHelpers.get(i);
                //' Update after all ConstraintHelper notification layouts are iterated '
                helper.updatePostLayout(this); }}... }public final void didMeasures(a) {... helperCount =this.layout.mConstraintHelpers.size();
            if (helperCount > 0) {
                for(int i = 0; i < helperCount; ++i) {
                    ConstraintHelper helper = (ConstraintHelper)this.layout.mConstraintHelpers.get(i);
                    //' Update after all ConstraintHelper notifications are measured '
                    helper.updatePostMeasure(this.layout); }}... }}Copy the code

ConstraintHelper tells ConstraintHelper to do various things at various times, as determined by the ConstraintHelper subclass, related to its associated control.

Get associated controls

ConstraintHelper uses the constraint_referenced_ids attribute in XML to associate controls. How is this attribute resolved in code?

public abstract class ConstraintHelper extends View {
    //' associated control id'
    protected int[] mIds = new int[32];
    //' associated control reference '
    private View[] mViews = null;
    
    public ConstraintHelper(Context context) {
        super(context);
        this.myContext = context;
        //' initialize '
        this.init((AttributeSet)null);
    }
    
    protected void init(AttributeSet attrs) {
        if(attrs ! =null) {
            TypedArray a = this.getContext().obtainStyledAttributes(attrs, styleable.ConstraintLayout_Layout);
            int N = a.getIndexCount();

            for(int i = 0; i < N; ++i) {
                int attr = a.getIndex(i);
                //' Get the value of constraint_referenced_ids'
                if (attr == styleable.ConstraintLayout_Layout_constraint_referenced_ids) {
                    this.mReferenceIds = a.getString(attr);
                    this.setIds(this.mReferenceIds); }}}}private void setIds(String idList) {
        if(idList ! =null) {
            int begin = 0;
            this.mCount = 0;

            while(true) {
                //' separate associated control ids by commas'
                int end = idList.indexOf(44, begin);
                if (end == -1) {
                    this.addID(idList.substring(begin));
                    return;
                }

                this.addID(idList.substring(begin, end));
                begin = end + 1; }}}private void addID(String idString) {
        if(idString ! =null&& idString.length() ! =0) {
            if (this.myContext ! =null) {
                idString = idString.trim();
                int rscId = 0;
                
                // get the Int value of the associated control id
                try {
                    Class res = id.class;
                    Field field = res.getField(idString);
                    rscId = field.getInt((Object)null);
                } catch (Exception var5) {
                }
                ...

                if(rscId ! =0) {
                    this.mMap.put(rscId, idString);
                    // add the associated control id to the array
                    this.addRscID(rscId); }... }}}private void addRscID(int id) {
        if (this.mCount + 1 > this.mIds.length) {
            this.mIds = Arrays.copyOf(this.mIds, this.mIds.length * 2);
        }
        // add the associated control id to the array
        this.mIds[this.mCount] = id;
        ++this.mCount; }}Copy the code

ConstraintHelper reads the value taken from the definition attribute constraint_referenced_ids, which is then comma-delimited and converted to an int value, and is finally present in int[] mIds. The purpose of this is to get an instance of the associated control View if necessary:

public abstract class ConstraintHelper extends View {
    protected View[] getViews(ConstraintLayout layout) {
        if (this.mViews == null || this.mViews.length ! =this.mCount) {
            this.mViews = new View[this.mCount];
        }
        //' traverse the array of associated control ids'
        for(int i = 0; i < this.mCount; ++i) {
            int id = this.mIds[i];
            //' convert id to View and store in array '
            this.mViews[i] = layout.getViewById(id);
        }

        return this.mViews; }}public class ConstraintLayout extends ViewGroup {
    //'ConstraintLayout temporary array of child controls'
    SparseArray<View> mChildrenByIds = new SparseArray();
    public View getViewById(int id) {
        return (View)this.mChildrenByIds.get(id);
    }
Copy the code

ConstraintHelper. GetViews () array traversal associated controls id and associated controls were obtained through the parent View.

Apply associated controls

ConstraintHelper. GetViews () method is protected, this means ConstraintHelper subclass will use this method, take a look at the in the Layer:

public class Layer extends ConstraintHelper {
    protected void calcCenters(a) {... View[] views =this.getViews(this.mContainer);
                    int minx = views[0].getLeft();
                    int miny = views[0].getTop();
                    int maxx = views[0].getRight();
                    int maxy = views[0].getBottom();
                    
                    //' traverse the associated control '
                    for(int i = 0; i < this.mCount; ++i) {
                        View view = views[i];
                        //' record the boundary of the associated control '
                        minx = Math.min(minx, view.getLeft());
                        miny = Math.min(miny, view.getTop());
                        maxx = Math.max(maxx, view.getRight());
                        maxy = Math.max(maxy, view.getBottom());
                    }
                    
                    //' record the associated control boundary in a member variable '
                    this.mComputedMaxX = (float)maxx;
                    this.mComputedMaxY = (float)maxy;
                    this.mComputedMinX = (float)minx;
                    this.mComputedMinY = (float)miny; . }}Copy the code

After the Layer obtains the boundary value of the associated control, it determines its own rectangle area according to the layout:

public class Layer extends ConstraintHelper {
    public void updatePostLayout(ConstraintLayout container) {...this.calcCenters();
        int left = (int)this.mComputedMinX - this.getPaddingLeft();
        int top = (int)this.mComputedMinY - this.getPaddingTop();
        int right = (int)this.mComputedMaxX + this.getPaddingRight();
        int bottom = (int)this.mComputedMaxY + this.getPaddingBottom();
        //' Define your own rectangle '
        this.layout(left, top, right, bottom);
        if(! Float.isNaN(this.mGroupRotateAngle)) {
            this.transform(); }}}Copy the code

This is why Layer can set the background for a set of associated controls.

ConstraintHelperIn order toConstraintLayoutThe identity of the child control appears in the layout file, and it associates other controls at the same level with custom properties. It is like a tag. When the parent control encounters a tag, it can do special things for the tagged control, such as “add a background to a set of child controls,” and these special things are defined in theConstraintHelperIn a subclass of.

Custom container controls

Aren’t we looking for a way to tell the parent control which child controls need to draw a red dot? This can be done by means of ConstraintHelper. After successful implementation, the layout file should look like this (pseudocode) :

<TreasureBox            
    xmlns:android="http://schemas.android.com/apk/res/android">

    <TextView
        android:id="@+id/tv"/>

    <Button
        android:id="@+id/btn"/>

    <ImageView
        android:id="@+id/iv"/>//' Draw a little red dot for TV, BTN, iv '<RedPointTreasure
        app:reference_ids="tv,btn,iv"/>

</TreasureBox>
Copy the code

TreasureBox and RedPointTreasure are the custom container controls and markup controls we will implement.

Write a custom container control modeled after ContraintLayout:

class TreasureBox @JvmOverloads 
    constructor(context: Context, attrs: AttributeSet? = null, defStyleAttr: Int = 0) :
    ConstraintLayout(context, attrs, defStyleAttr) {
    //' Mark control list '
    private var treasures = mutableListOf<Treasure>()
    init {
        //' This line of code is necessary, otherwise you cannot paint on the container control canvas'
        setWillNotDraw(false)}//' Filter out marked controls and save references when child controls are added '
    override fun onViewAdded(child: View?). {
        super.onViewAdded(child)
        (child as? Treasure)? .let { treasure -> treasures.add(treasure) } }//' Filter out marker controls and remove references when child controls are removed '
    override fun onViewRemoved(child: View?). {
        super.onViewRemoved(child)
        (child as? Treasure)? .let { treasure -> treasures.remove(treasure) } }//' When the container control foreground is drawn, the notification marker control is drawn '
    override fun onDrawForeground(canvas: Canvas?). {
        super.onDrawForeground(canvas)
        treasures.forEach { treasure -> treasure.drawTreasure(this, canvas) }
    }
}
Copy the code

Because the little red dot is drawn on the container control canvas, we must call setWillNotDraw(false) on initialization. This function is used to determine whether the control’s current view will draw:

public class View {
    
    //' Control sets this flag to indicate that it will not draw itself '
    static final int WILL_NOT_DRAW = 0x00000080;
    
    //' If the view does not draw its own content, set this flag to false'
    public void setWillNotDraw(boolean willNotDraw) {
        setFlags(willNotDraw ? WILL_NOT_DRAW : 0, DRAW_MASK); }}Copy the code

The container control ViewGroup defaults to false:

public abstract class ViewGroup extends View {
    private void initViewGroup(a) {
        // ViewGroup doesn’t draw by default
        //' By default, container controls do not draw on their own canvas'
        if(! debugDraw()) { setFlags(WILL_NOT_DRAW, DRAW_MASK); }... }}Copy the code

The onDraw() function can also draw a red dot, but when the child control set the background color, the red dot is overwritten, look back at the source to find that onDraw() draws the content of the control itself, and dispatchDraw() draws the content of the child control after it. The later the drawing is, the higher it is:

public class View {
    public void draw(Canvas canvas) {...if(! verticalEdges && ! horizontalEdges) {//' Draw yourself '
            onDraw(canvas);

            //' Draw child '
            dispatchDraw(canvas);

            drawAutofilledHighlight(canvas);

            // Overlay is part of the content and draws beneath Foreground
            if(mOverlay ! =null && !mOverlay.isEmpty()) {
                mOverlay.getOverlayView().dispatchDraw(canvas);
            }

            //' draw foreground '
            onDrawForeground(canvas);

            // Step 7, draw the default focus highlight
            drawDefaultFocusHighlight(canvas);

            if (debugDraw()) {
                debugDrawFocus(canvas);
            }

            return; }... }Copy the code

Draw foreground after the child is drawn, so drawing foreground in onDrawForeground() ensures that the little red dot won’t be overwritten by the quilt control. A detailed explanation of control drawing can be found here.

Custom tag controls

Write a custom tag control that mimics ConstraintHelper:

abstract class Treasure @JvmOverloads 
    constructor(context: Context, attrs: AttributeSet? = null, defStyleAttr: Int = 0) : 
    View(context, attrs, defStyleAttr) {
    //' list of associated ids'
    internal var ids = mutableListOf<Int> ()//' Parse custom data at build time '
    init {
        readAttrs(attrs)
    }

    //' marks the place where the control draws concrete content for subclass implementation (canvas is the canvas of the container control) '
    abstract fun drawTreasure(treasureBox: TreasureBox, canvas: Canvas?). 

    //' parse custom attribute 'association ID'
    open fun readAttrs(attributeSet: AttributeSet?).{ attributeSet? .let { attrs -> context.obtainStyledAttributes(attrs, R.styleable.Treasure)? .let { divideIds(it.getString(R.styleable.Treasure_reference_ids)) it.recycle() } } }//' Parse the associative ID as a string into an int to get the control reference with findViewById() '
    private fun divideIds(idString: String?).{ idString? .split(",")? .forEach { id -> ids.add(resources.getIdentifier(id.trim(),"id", context.packageName))
        }
    }
}
Copy the code

This is the base class of the custom tag control. This abstraction is used to resolve the base attribute of the tag control, “association ID”, as defined below:


      
<resources>
    <declare-styleable name="Treasure">
        <attr name="reference_ids" format="string" />
    </declare-styleable>
</resources>
Copy the code

The drawing function is abstract, and the concrete drawing logic is handed over to subclasses:

class RedPointTreasure @JvmOverloads 
    constructor(context: Context, attrs: AttributeSet? = null, defStyleAttr: Int = 0) :
    Treasure(context, attrs, defStyleAttr) {
    
    private val DEFAULT_RADIUS = 5F
    //' red dot center x offset '
    private lateinit var offsetXs: MutableList<Float>
    //' y offset '
    private lateinit var offsetYs: MutableList<Float>
    //' red dot radius'
    private lateinit var radiuses: MutableList<Float>
    //' red dot '
    private var bgPaint: Paint = Paint()

    init {
        initPaint()
    }
    
    //' initialize brush '
    private fun initPaint(a) {
        bgPaint.apply {
            isAntiAlias = true
            style = Paint.Style.FILL
            color = Color.parseColor("#ff0000")}}//' Parse custom attributes'
    override fun readAttrs(attributeSet: AttributeSet?). {
        super.readAttrs(attributeSet) attributeSet? .let { attrs -> context.obtainStyledAttributes(attrs, R.styleable.RedPointTreasure)? .let { divideRadiuses(it.getString(R.styleable.RedPointTreasure_reference_radius)) dividerOffsets( it.getString(R.styleable.RedPointTreasure_reference_offsetX), it.getString(R.styleable.RedPointTreasure_reference_offsetY) ) it.recycle() } } }//' red dot draw logic '
    override fun drawTreasure(treasureBox: TreasureBox, canvas: Canvas?). {
        //' walk through the association id list 'ids.forEachIndexed { index, id -> treasureBox.findViewById<View>(id)? .let { v ->val cx = v.right + offsetXs.getOrElse(index) { 0F }.dp2px()
                val cy = v.top + offsetYs.getOrElse(index) { 0F }.dp2px()
                valradius = radiuses.getOrElse(index) { DEFAULT_RADIUS }.dp2px() canvas? .drawCircle(cx, cy, radius, bgPaint) } } }//' resolve offset '
    private fun dividerOffsets(offsetXString: String? , offsetYString:String?).{ offsetXs = mutableListOf() offsetYs = mutableListOf() offsetXString? .split(",")? .forEach { offset -> offsetXs.add(offset.trim().toFloat()) } offsetYString? .split(",")? .forEach { offset -> offsetYs.add(offset.trim().toFloat()) } }//' resolve radius'
    private fun divideRadiuses(radiusString: String?).{ radiuses = mutableListOf() radiusString? .split(",")? .forEach { radius -> radiuses.add(radius.trim().toFloat()) } }//' red dot size multi-screen fit '
    private fun Float.dp2px(a): Float {
        val scale = Resources.getSystem().displayMetrics.density
        return this * scale + 0.5 f}}Copy the code

The parsed custom attributes are as follows:


      
<resources>
    <declare-styleable name="RedPointTreasure">
        <attr name="reference_radius" format="string" />
        <attr name="reference_offsetX" format="string" />
        <attr name="reference_offsetY" format="string" />
    </declare-styleable>
</resources>
Copy the code

Then you can complete the drawing of the red dot in the XML file, and the effect picture is as follows:

XML is defined as follows:


      
<TreasureBox 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"
    tools:context=".MainActivity">

    <TextView
        android:id="@+id/tv"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:padding="10dp"
        android:text="Message"
        android:textSize="20sp"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintEnd_toStartOf="@id/btn"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toTopOf="parent" />

    <Button
        android:id="@+id/btn"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:padding="10dp"
        android:text="Mail box"
        android:textSize="20sp"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintEnd_toStartOf="@id/tv"
        app:layout_constraintStart_toEndOf="@id/iv"
        app:layout_constraintTop_toTopOf="parent" />

    <ImageView
        android:id="@+id/iv"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:padding="10dp"
        android:src="@drawable/ic_voice_call"
        android:textSize="20sp"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintStart_toEndOf="@id/btn"
        app:layout_constraintTop_toTopOf="parent" />

    <RedPointTreasure
        android:id="@+id/redPoint"
        android:layout_height="wrap_content"
        android:layout_width="wrap_content"//' are child controlstv.btn.ivDraw little red dot 'app:reference_ids="tv,btn,iv"/ / 'tv.btn.ivThe radii of the little red dot are PI5.13.8'
        app:reference_radius="Five,13,8"/ / 'tv.btn.ivThe little red dotxThe offsets are, respectively10.0.0'
        app:reference_offsetY="10 0,"/ / 'tv.btn.ivThe little red dotyThe offsets are, respectively- 10.0.0'
        app:reference_offsetX="-10,0,0"
        />

</TreasureBox>
Copy the code

The business layer usually needs to dynamically change the display state of the red dot to add an interface to RedPointTreasure:

class RedPointTreasure @JvmOverloads 
    constructor(context: Context, attrs: AttributeSet? = null, defStyleAttr: Int = 0) :
    Treasure(context, attrs, defStyleAttr) {
    
    companion object {
        @JvmStatic
        val TYPE_RADIUS = "radius"
        @JvmStatic
        val TYPE_OFFSET_X = "offset_x"
        @JvmStatic
        val TYPE_OFFSET_Y = "offset_y"
    }
    
    //' Set custom properties for specified associated controls'
    fun setValue(id: Int, type: String, value: Float) {
        val dirtyIndex = ids.indexOf(id)
        if(dirtyIndex ! = -1) {
            when (type) {
                TYPE_OFFSET_X -> offsetXs[dirtyIndex] = value
                TYPE_OFFSET_Y -> offsetYs[dirtyIndex] = value
                TYPE_RADIUS -> radiuses[dirtyIndex] = value
            }
            //' trigger redraw of parent control '
            (parent as? TreasureBox)? .postInvalidate() } } }Copy the code

To hide the little red dot, just set the radius to 0:

redPoint? .setValue(R.id.tv, RedPointTreasure.TYPE_RADIUS,0f)
Copy the code

This combination of container controls and tag controls can do a lot more than just draw red dots. This is how a set of child controls and parent controls communicate with each other.

talk is cheap, show me the code

The full source code can be found here

Recommended reading

This is also the third article in the long knowledge series of read source code, which is characterized by the application of design ideas in the source code to real projects, the list of articles in the series is as follows:

  1. Read the source code long knowledge better RecyclerView | click listener

  2. Android custom controls | source there is treasure in the automatic line feed control

  3. Android custom controls | three implementation of little red dot (below)

  4. Reading knowledge source long | dynamic extension class and bind the new way of the life cycle

  5. Reading knowledge source long | Android caton true because “frame”?