The previous article showed red dots by drawing a foreground on the parent control. Configuring the marker control in the layout file adds red dots to any child control. The implementation scheme is “configure the control ID with a small red dot in the layout file, get their coordinates in the parent control, and draw a circle in its upper right corner”. But this scheme has a bug, when the child control animation, that is, when the child control size changes, the little red dot will not link. The effect is shown below:

This is the seventh 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)

So the new topic is:How do I listen for and respond to child control redraw in a parent control?

Listen to redraw

Draw(), dispatchDraw(), drawChild(), child control animation can not capture the linkage event.

Suddenly remembered androidx. Coordinatorlayout. Widget. The behaviors of coordinatorlayout in onDependentViewChanged () can acquire the properties of the associated control real-time changes. How does it do it? Look up the call chain:

public class CoordinatorLayout extends ViewGroup{
    final void onChildViewsChanged(@DispatchChangeEvent final int type) {
        final int childCount = mDependencySortedChildren.size();

        for (int i = 0; i < childCount; i++) {
            final View child = mDependencySortedChildren.get(i);

            //' Traverse all dependent child controls'
            for (int j = i + 1; j < childCount; j++) {
                finalView checkChild = mDependencySortedChildren.get(j); .if(b ! =null && b.layoutDependsOn(this, checkChild, child)) {
                    ...
                    final boolean handled;
                    switch (type) {
                        case EVENT_VIEW_REMOVED:
                            // EVENT_VIEW_REMOVED means that we need to dispatch
                            // onDependentViewRemoved() instead
                            b.onDependentViewRemoved(this, checkChild, child);
                            handled = true;
                            break;
                        default:
                            //' Pass child control changes'
                            handled = b.onDependentViewChanged(this, checkChild, child);
                            break; }... }}}}Copy the code

When the associated child control changes, the associated control is iterated over and the transformation is passed onDependentViewChanged(). Further up the call chain:

public class CoordinatorLayout extends ViewGroup{
    class OnPreDrawListener implements ViewTreeObserver.OnPreDrawListener {
        @Override
        public boolean onPreDraw(a) {
            //' Capture child control property change events in onPreDraw() '
            onChildViewsChanged(EVENT_PRE_DRAW);
            return true; }}@Override
    public void onAttachedToWindow(a) {
        super.onAttachedToWindow();
        if (mNeedsPreDrawListener) {
            if (mOnPreDrawListener == null) {
                //' Build PreDrawListener in onAttachedToWindow() '
                mOnPreDrawListener = new OnPreDrawListener();
            }
            final ViewTreeObserver vto = getViewTreeObserver();
            //' Register a View tree observer 'vto.addOnPreDrawListener(mOnPreDrawListener); }}}//' Global View tree observer '
public final class ViewTreeObserver {
    public interface OnPreDrawListener {
        // this interface is called before the view tree is drawn, and all views in the view tree are mapped by measure and Layout.
        public boolean onPreDraw(a); }}Copy the code

CoordinatorLayout registers the View tree observer on onAttachedToWindow(), which must redraw the View tree if the child controls’ properties change so that they can listen for their property changes in onPreDraw().

Copy this mechanism to custom container control TreasureBox:

// A custom container control must be used with the tag control
class TreasureBox @JvmOverloads constructor(context: Context, attrs: AttributeSet? = null, defStyleAttr: Int = 0) :
    ConstraintLayout(context, attrs, defStyleAttr) {
    //' Marks a list of controls to mark which child controls need a red dot '
    private var treasures = mutableListOf<Treasure>()
    //'View tree observer '
    private var onPreDrawListener: ViewTreeObserver.OnPreDrawListener = ViewTreeObserver.OnPreDrawListener {
        //' Notify all markup controls before View tree redraws'
        treasures.forEach { treasure -> treasure.onPreDraw(this)}true
    }

    override fun onViewAdded(child: View?). {
        super.onViewAdded(child)
        // Store markup controls
        (child as? Treasure)? .let { treasure -> treasures.add(treasure) } }override fun onViewRemoved(child: View?). {
        super.onViewRemoved(child)
        // Remove the tag control
        (child as? Treasure)? .let { treasure -> treasures.remove(treasure) } }override fun onAttachedToWindow(a) {
        super.onAttachedToWindow()
        //' Register the View tree listener '
        viewTreeObserver.addOnPreDrawListener(onPreDrawListener)
    }

Copy the code

This allows the marker control to be notified in onPreDraw() when the property of the child control that needs to draw the little red dot changes:

//' abstract tag control '
abstract class Treasure @JvmOverloads constructor(context: Context, attrs: AttributeSet? = null, defStyleAttr: Int = 0) :
    View(context, attrs, defStyleAttr) {
    //' Associated control ID list '
    internal var ids = mutableListOf<Int> ()fun onPreDraw(treasureBox: TreasureBox) {
        ids.map { treasureBox.findViewById<View>(it) }.forEach { v ->
            //' Here can listen for associated child control property changes'}}Copy the code

Child control redraw drives parent control redraw

The width, height and coordinates of the child controls can be obtained in onPreDraw() in real time before each View tree redrawing. In order to avoid excessive redrawing, the parent control will be redrawn only when the property changes. Need to remember the last redrawn attribute, through comparison can know whether the attribute has changed:

abstract class Treasure @JvmOverloads constructor(context: Context, attrs: AttributeSet? = null, defStyleAttr: Int = 0) :
    View(context, attrs, defStyleAttr) {
    //' Associated control property, corresponding to associated control ID list '
    var layoutParams = mutableListOf<LayoutParam>()
    //' Associated control ID list '
    internal var ids = mutableListOf<Int> ()fun onPreDraw(treasureBox: TreasureBox) {
        //' Iterate over associated controls to see if their properties have changed before they are redrawn 'ids.forEachIndexed { index, id -> treasureBox.findViewById<View>(id)? .let { v -> LayoutParam(v.width, v.height, v.x, v.y).let { lp ->// trigger parent control redraw if associated control property changes
                    if(layoutParams[index] ! = lp) {if (layoutParams[index].isValid()) {
                            treasureBox.postInvalidate()
                        }
                        layoutParams[index] = lp
                    }
                }
            }
        }
    }
        
    //' control property entity class, store width, height and coordinates'
    data class LayoutParam(var width: Int = 0.var height: Int = 0.var x: Float = 0f.var y: Float = 0f) {
        private var id: Int? = null
        override fun equals(other: Any?).: Boolean {
            if (other == null || other !is LayoutParam) return false
            //' Attributes are considered unchanged only when all attributes are the same '
            return width == other.width && height == other.height && x == other.x && y == other.y
        }

        fun isValid(a)= width ! =0&& height ! =0}}Copy the code

Also need to change the little red dot drawing logic, the previous logic is as follows:

//' red dot control '
class RedPointTreasure @JvmOverloads constructor(context: Context, attrs: AttributeSet? = null, defStyleAttr: Int = 0) :
    Treasure(context, attrs, defStyleAttr) {
    
    override fun drawTreasure(treasureBox: TreasureBox, canvas: Canvas?). {
        //' Traverse the associated control and draw a small red dot at the corresponding position on the parent control canvas'ids.forEachIndexed { index, id -> treasureBox.findViewById<View>(id)? .let { v ->//' Determine the abscissa of the red dot by the right value of the associated control '
                val cx = v.right + v.width + offsetXs.getOrElse(index) { 0F }.dp2px()
                //' Determine the vertical coordinate of the red dot by the top value of the associated control '
                val cy = v.top + offsetYs.getOrElse(index) { 0F }.dp2px()
                valradius = radiuses.getOrElse(index) { DEFAULT_RADIUS }.dp2px() canvas? .drawCircle(cx, cy, radius, bgPaint) } } } }Copy the code

If you use this drawing logic, even if the parent control listens to the child control redraw, the little red dot will not follow. That’s because View getTop() and getRight() don’t contain displacement values:

public class View{
    public final int getTop(a) {
        return mTop;
    }
    
    public final int getRight(a) {
        returnmRight; }}Copy the code

GetX () and getY() contain displacement values:

public class View{
    public float getX(a) {
        return mLeft + getTranslationX();
    }
    
    public float getY(a) {
        returnmTop + getTranslationY(); }}Copy the code

Just replace v.light and v.tab in the drawing logic with v.x and v.y, and the little red dot will link with the animation. Add a shift and scale animation to the control to test it:

GG Smida. Shift animations do link, but scaling does not

When a View is animated by setScale(), its width, height and coordinates do not change…

But there must have been a change in the value of one of the properties, although I don’t know what it was, right?

You can only open the View source, iterate over all the functions that start with get, and print their values in onPreDraw(). After many attempts, I finally found a function whose return value is linked to the zooming animation of the child control:

public class View{
    public void getHitRect(Rect outRect) {
        if (hasIdentityMatrix() || mAttachInfo == null) {
            outRect.set(mLeft, mTop, mRight, mBottom);
        } else {
            final RectF tmpRect = mAttachInfo.mTmpTransformRect;
            tmpRect.set(0.0, getWidth(), getHeight());
            //' take matrix into account '
            getMatrix().mapRect(tmpRect)
            outRect.set((int) tmpRect.left + mLeft, (int) tmpRect.top + mTop,
                    (int) tmpRect.right + mLeft, (int) tmpRect.bottom + mTop); }}}Copy the code

When the child control is zoomed out, the left in the Rect returned by this function becomes larger and the right becomes smaller.

The return value of a function in mLeft, mRight, mTop, mBottom superposition on the basis of the value of the matrix. Animation property values are eventually reflected in the matrix, which seems to support the analysis, that is, the function will return the view property values changed by animation in real time.

This way, just by remembering the last Rect, we can tell if the child control was animated by comparison before the next redraw:

// Marks the control
abstract class Treasure @JvmOverloads constructor(context: Context, attrs: AttributeSet? = null, defStyleAttr: Int = 0) :
    View(context, attrs, defStyleAttr) {
    // Associate a list of child control ids
    internal var ids = mutableListOf<Int> ()//' List of current frames associated with child controls'
    var rects = mutableListOf<Rect>()
    //' Associate a frame list of regions on a child control '
    var lastRects = mutableListOf<Rect>()
    
    fun onPreDraw(treasureBox: TreasureBox) {
        //' traverse the associated control 'ids.forEachIndexed { index, id -> treasureBox.findViewById<View>(id)? .let { v ->//' Get current frame control area '
                v.getHitRect(rects[index])
                //' If the current frame control area changes, inform the parent control to redraw '
                if(rects[index] ! = lastRects[index]) { treasureBox.postInvalidate()//' Update previous frame control area '
                    lastRects[index].set(rects[index])
                }
            }
        }
    }
    
    // Parsing XML reads the associated child control 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() } } }//' split associated child control ID string '
    private fun divideIds(idString: String?).{ idString? .split(",")? .forEach { id -> ids.add(resources.getIdentifier(id.trim(),"id", context.packageName))
            // initialize the current frame region for each associated child control
            rects.add(Rect())
            // initialize the previous frame region for each associated child control
            lastRects.add(Rect())
        }
        ids.toCollection(mutableListOf()).print("ids") { it.toString() }
    }
}
Copy the code

Drawing the red dot logic also needs to be changed in response:

class RedPointTreasure @JvmOverloads constructor(context: Context, attrs: AttributeSet? = null, defStyleAttr: Int = 0) :
    Treasure(context, attrs, defStyleAttr) {

    //' Draw a small red dot in the foreground of the parent control canvas'
    override fun drawTreasure(treasureBox: TreasureBox, canvas: Canvas?).{ ids.forEachIndexed { index, id -> treasureBox.findViewById<View>(id)? .let { v ->//' the abscissa of the little red dot is dependent on the right margin of the current frame area '
                val cx = rects[index].right + offsetXs.getOrElse(index) { 0F }.dp2px()
                //' the center of the red dot depends on the upper boundary of the current frame region '
                val cy = rects[index].top + offsetYs.getOrElse(index) { 0F }.dp2px()
                valradius = radiuses.getOrElse(index) { DEFAULT_RADIUS }.dp2px() canvas? .drawCircle(cx, cy, radius, bgPaint) } } }Copy the code

The result is as follows:

talk is cheap, show me the code