Android自定义控件 | 小红点的三种实现(下)

本文涉及的产品
全局流量管理 GTM,标准版 1个月
云解析 DNS,旗舰版 1个月
公共DNS(含HTTPDNS解析),每月1000万次HTTP解析
简介: 上篇介绍了两种实现小红点的方案,分别是多控件叠加和单控件绘制,其中第二个方案有一个缺点:类型绑定。导致它无法被不同类型控件所复用。这篇给出一种新的方案。

此文标题想了好久久久,本起名为《读原码长知识 | 小红点的一种实现》,但纠结了下,觉得还是应该隶属于自定义控件系列~~

上篇介绍了两种实现小红点的方案,分别是多控件叠加和单控件绘制,其中第二个方案有一个缺点:类型绑定。导致它无法被不同类型控件所复用。这篇从父控件的角度出发,提出一个新的方案:容器控件绘制,以突破类型绑定。

这是自定义控件系列教程的第六篇,系列文章目录如下:

  1. Android自定义控件 | View绘制原理(画多大?)
  2. Android自定义控件 | View绘制原理(画在哪?)
  3. Android自定义控件 | View绘制原理(画什么?)
  4. Android自定义控件 | 源码里有宝藏之自动换行控件
  5. Android自定义控件 | 小红点的三种实现(上)
  6. Android自定义控件 | 小红点的三种实现(下)
  7. Android自定义控件 | 小红点的三种实现(终结)

引子

假设这样一个场景:一个容器控件中,有三种不同类型的控件需要在右上角显示小红点。若使用上一篇中的“单控件绘制方案”,就必须自定义三种不同类型的控件,在其矩形区域的右上角绘制小红点。

可不可以把绘制工作交给容器控件?

容器控件能轻而易举地知道子控件矩形区域的坐标,有什么办法把“哪些孩子需要绘制小红点”告诉容器控件,以让其在相应位置绘制?

在读androidx.constraintlayout.helper.widget.Layer源码时,发现它用一种巧妙的方式将子控件的信息告诉容器控件。

Layer的启发

绑定关联控件

Layer是一个配合ConstraintLayout使用的控件,可实现如下效果:


即在不增加布局层级的情况下,为一组子控件设置背景,代码如下:

<?xml version="1.0" encoding="utf-8"?>
<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" />
    
    //'为3个button添加背景'
    <androidx.constraintlayout.helper.widget.Layer
        android:id="@+id/layer"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:background="#0000ff"
        //'关联3个button'
        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>

LayerButton平级,只使用了属性app:constraint_referenced_ids="btn3,btn4,btn5"标记关联控件就能为其添加背景,很好奇是怎么做到的,点开源码:

public class Layer extends ConstraintHelper {}

public abstract class ConstraintHelper extends View {}

LayerConstraintHelper的子类,而ConstraintHelper是自定义View。所以它可以在 xml 中被声明为ConstraintLayout的子控件。

想必ConstraintLayout遍历子控件时会将ConstraintHelper存储起来。在ConstraintLayout中搜索ConstraintHelper,果不其然:

public class ConstraintLayout extends ViewGroup {
    //'存储ConstraintHelper的列表'
    private ArrayList<ConstraintHelper> mConstraintHelpers = new ArrayList(4);
    
    //'当子控件被添加到容器时该方法被调用'
    public void onViewAdded(View view) {
        ...
        //'存储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);
            }
        }
        ...
    }
}

有添加必有移除,应该有一个和onViewAdded()对应的方法:

public class ConstraintLayout extends ViewGroup {
    //'当子控件被移除到容器时该方法被调用'
    public void onViewRemoved(View view) {
        ...
        this.mChildrenByIds.remove(view.getId());
        ConstraintWidget widget = this.getViewWidget(view);
        this.mLayoutWidget.remove(widget);
        //'将ConstraintHelper子控件移除'
        this.mConstraintHelpers.remove(view);
        this.mVariableDimensionsWidgets.remove(widget);
        this.mDirtyHierarchy = true;
    }
}

除了这两处,ConstraintLayout中和ConstraintHelper相关的代码并不多:

public class ConstraintLayout extends ViewGroup {
    private void setChildrenConstraints() {
        ...
        helperCount = this.mConstraintHelpers.size();
        int i;
        if (helperCount > 0) {
            for(i = 0; i < helperCount; ++i) {
                ConstraintHelper helper = (ConstraintHelper)this.mConstraintHelpers.get(i);
                //'遍历所有ConstraintHelper通知布局前更新'
                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);
                //'遍历所有ConstraintHelper通知布局后更新'
                helper.updatePostLayout(this);
            }
        }
        ...
    }
    public final void didMeasures() {
            ...
            helperCount = this.layout.mConstraintHelpers.size();
            if (helperCount > 0) {
                for(int i = 0; i < helperCount; ++i) {
                    ConstraintHelper helper = (ConstraintHelper)this.layout.mConstraintHelpers.get(i);
                    //'遍历所有ConstraintHelper通知测量后更新'
                    helper.updatePostMeasure(this.layout);
                }
            }
            ...
    }
}

都是在各种时机通知ConstraintHelper做各种事情,这些事情和它的关联控件有关,具体做什么由ConstraintHelper子类决定。

获取关联控件

ConstraintHelper在 xml 中使用constraint_referenced_ids属性来关联控件,代码中是如何解析该属性的?

public abstract class ConstraintHelper extends View {
    //'关联控件id'
    protected int[] mIds = new int[32];
    //'关联控件引用'
    private View[] mViews = null;
    
    public ConstraintHelper(Context context) {
        super(context);
        this.myContext = context;
        //'初始化'
        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);
                //'获取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) {
                //'将关联控件id按逗号分隔'
                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;
                
                //'获取关联控件id的Int值'
                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);
                    //'将关联控件id加入数组'
                    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);
        }
        //'将关联控件id加入数组'
        this.mIds[this.mCount] = id;
        ++this.mCount;
    }
}

ConstraintHelper先读取自定义属性constraint_referenced_ids的值,然后将其按逗号分隔并转换成 int 值,最终存在int[] mIds中。这样做的目的是为了在必要时获取关联控件 View 的实例:

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];
        }
        //'遍历关联控件id数组'
        for(int i = 0; i < this.mCount; ++i) {
            int id = this.mIds[i];
            //'将id转换成View并存入数组'
            this.mViews[i] = layout.getViewById(id);
        }

        return this.mViews;
    }
}

public class ConstraintLayout extends ViewGroup {
    //'ConstraintLayout暂存子控件的数组'
    SparseArray<View> mChildrenByIds = new SparseArray();
    public View getViewById(int id) {
        return (View)this.mChildrenByIds.get(id);
    }

ConstraintHelper.getViews()遍历关联控件 id 数组并通过父控件获得关联控件 View 。

应用关联控件

ConstraintHelper.getViews()protected方法,这意味着ConstraintHelper的子类会用到这个方法,去Layer里看一下:

public class Layer extends ConstraintHelper {
    protected void calcCenters() {
                    ...
                    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();
                    
                    //'遍历关联控件'
                    for(int i = 0; i < this.mCount; ++i) {
                        View view = views[i];
                        //'记录关联控件控件的边界'
                        minx = Math.min(minx, view.getLeft());
                        miny = Math.min(miny, view.getTop());
                        maxx = Math.max(maxx, view.getRight());
                        maxy = Math.max(maxy, view.getBottom());
                    }
                    
                    //'将关联控件边界记录在成员变量中'
                    this.mComputedMaxX = (float)maxx;
                    this.mComputedMaxY = (float)maxy;
                    this.mComputedMinX = (float)minx;
                    this.mComputedMinY = (float)miny;
                    ...
    }
}

Layer在获得关联控件边界值之后,会在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();
        //'确定自己的矩形区域'
        this.layout(left, top, right, bottom);
        if (!Float.isNaN(this.mGroupRotateAngle)) {
            this.transform();
        }
    }
}

这就是为啥Layer可以为一组关联控件设置背景的原因。

ConstraintHelperConstraintLayout子控件的身份出现在布局文件中,它通过自定义属性来关联同级的其他控件,它就好像一个标记,当父控件遇到标记时,就能为被标记的控件做一些特殊的事情,比如“为一组子控件添加背景”,而这些特殊的事情就定义在ConstraintHelper的子类中。

自定义容器控件

我们不是正在寻找“如何把哪些子控件需要绘制小红点告诉父控件”的方法吗?借用ConstraintHelper的思想方法就能实现。实现成功之后的布局文件应该长这样(伪码):

<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"/>

    //'为tv,btn,iv绘制小红点'
    <RedPointTreasure
        app:reference_ids="tv,btn,iv"/>

</TreasureBox>

其中的TreasureBoxRedPointTreasure就是我们要实现的自定义容器控件和标记控件。

仿照ContraintLayout写一个自定义容器控件:

class TreasureBox @JvmOverloads 
    constructor(context: Context, attrs: AttributeSet? = null, defStyleAttr: Int = 0) :
    ConstraintLayout(context, attrs, defStyleAttr) {
    //'标记控件列表'
    private var treasures = mutableListOf<Treasure>()
    init {
        //'这行代码是必须的,否则不能在容器控件画布绘制图案'
        setWillNotDraw(false)
    }
    
    //'当子控件被添加时,过滤出标记控件并保存引用'
    override fun onViewAdded(child: View?) {
        super.onViewAdded(child)
        (child as? Treasure)?.let { treasure ->
            treasures.add(treasure)
        }
    }

    //'当子控件被移除时,过滤出标记控件并移除引用'
    override fun onViewRemoved(child: View?) {
        super.onViewRemoved(child)
        (child as? Treasure)?.let { treasure ->
            treasures.remove(treasure)
        }
    }

    //'绘制容器控件前景时,通知标记控件绘制'
    override fun onDrawForeground(canvas: Canvas?) {
        super.onDrawForeground(canvas)
        treasures.forEach { treasure -> treasure.drawTreasure(this, canvas) }
    }
}

因为小红点是绘制在容器控件画布上的,所以在初始化时必须调用setWillNotDraw(false),该函数用于控件当前视图是否会绘制:

public class View {
    
    //'控件设置了这个flag,则表示它不会自己绘制'
    static final int WILL_NOT_DRAW = 0x00000080;
    
    //'如果视图自己不绘制内容,则可以将这个flag为false'
    public void setWillNotDraw(boolean willNotDraw) {
        setFlags(willNotDraw ? WILL_NOT_DRAW : 0, DRAW_MASK);
    }
}

而容器控件ViewGroup默认将其设为了 false :

public abstract class ViewGroup extends View {
    private void initViewGroup() {
        // ViewGroup doesn’t draw by default
        //'默认情况下,容器控件都不会在自己画布上绘制'
        if (!debugDraw()) {
            setFlags(WILL_NOT_DRAW, DRAW_MASK);
        }
        ...
    }
}

一开始想当然地把绘制逻辑写在了onDraw()函数中,虽然也可以绘制出小红点,但当子控件设置背景色时,小红点就被覆盖了,回看源码才发现,onDraw()绘制的是控件自身的内容,而绘制子控件内容的dispatchDraw()在它之后,越晚绘制的就在越上层:

public class View {
    public void draw(Canvas canvas) {
        ...
        if (!verticalEdges && !horizontalEdges) {
            //'绘制自己'
            onDraw(canvas);

            //'绘制孩子'
            dispatchDraw(canvas);

            drawAutofilledHighlight(canvas);

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

            //'绘制前景'
            onDrawForeground(canvas);

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

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

            return;
        }
        ...
    }

绘制前景在绘制孩子之后,所以在onDrawForeground()中绘制可以保证小红点不会被子控件覆盖。

自定义标记控件

接着模仿ConstraintHelper写一个自定义标记控件:

abstract class Treasure @JvmOverloads 
    constructor(context: Context, attrs: AttributeSet? = null, defStyleAttr: Int = 0) : 
    View(context, attrs, defStyleAttr) {
    //'用于存放关联id的列表'
    internal var ids = mutableListOf<Int>()
    //'在构造时解析自定义数据'
    init {
        readAttrs(attrs)
    }

    //'标记控件绘制具体内容的地方,供子类实现(canvas是容器控件的画布)'
    abstract fun drawTreasure(treasureBox: TreasureBox, canvas: Canvas?) 

    //'解析自定义属性“关联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()
            }
        }
    }

    //'将字符串形式的关联id解析成int值,以便通过findViewById()获取控件引用'
    private fun divideIds(idString: String?) {
        idString?.split(",")?.forEach { id ->
            ids.add(resources.getIdentifier(id.trim(), "id", context.packageName))
        }
    }
}

这个是自定义标记控件的基类,这层抽象只是用来解析标记控件的基础属性“关联id”,定义如下:

<?xml version="1.0" encoding="utf-8"?>
<resources>
    <declare-styleable name="Treasure">
        <attr name="reference_ids" format="string" />
    </declare-styleable>
</resources>

绘制函数是抽象的,具体的绘制逻辑交给子类实现:

class RedPointTreasure @JvmOverloads 
    constructor(context: Context, attrs: AttributeSet? = null, defStyleAttr: Int = 0) :
    Treasure(context, attrs, defStyleAttr) {
    
    private val DEFAULT_RADIUS = 5F
    //'小红点圆心x偏移量'
    private lateinit var offsetXs: MutableList<Float>
    //'小红点圆心y偏移量'
    private lateinit var offsetYs: MutableList<Float>
    //'小红点半径'
    private lateinit var radiuses: MutableList<Float>
    //'小红点画笔'
    private var bgPaint: Paint = Paint()

    init {
        initPaint()
    }
    
    //'初始化画笔'    
    private fun initPaint() {
        bgPaint.apply {
            isAntiAlias = true
            style = Paint.Style.FILL
            color = Color.parseColor("#ff0000")
        }
    }

    //'解析自定义属性'
    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()
            }
        }
    }

    //'小红点绘制逻辑'
    override fun drawTreasure(treasureBox: TreasureBox, canvas: Canvas?) {
        //'遍历关联id列表'
        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()
                val radius = radiuses.getOrElse(index) { DEFAULT_RADIUS }.dp2px()
                canvas?.drawCircle(cx, cy, radius, bgPaint)
            }
        }
    }

    //'解析偏移量'
    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()) }
    }

    //'解析半径'
    private fun divideRadiuses(radiusString: String?) {
        radiuses = mutableListOf()
        radiusString?.split(",")?.forEach { radius -> radiuses.add(radius.trim().toFloat()) }
    }
    
    //'小红点尺寸多屏幕适配'
    private fun Float.dp2px(): Float {
        val scale = Resources.getSystem().displayMetrics.density
        return this * scale + 0.5f
    }
}

解析的自定义属性如下:

<?xml version="1.0" encoding="utf-8"?>
<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>

然后就可以在 xml 文件中完成小红点的绘制,效果图如下:


xml 定义如下:

<?xml version="1.0" encoding="utf-8"?>
<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"
        //'分别为子控件tv,btn,iv绘制小红点'
        app:reference_ids="tv,btn,iv"
        //'tv,btn,iv小红点的半径分别是5,13,8'
        app:reference_radius="5,13,8"
        //'tv,btn,iv小红点的x偏移量分别是10,0,0'
        app:reference_offsetY="10,0,0"
        //'tv,btn,iv小红点的y偏移量分别是-10,0,0'
        app:reference_offsetX="-10,0,0"
        />

</TreasureBox>

业务层通常需要动态改变小红点的显示状态,为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"
    }
    
    //'为指定关联控件设置自定义属性'
    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
            }
            //'触发父控件的重绘'
            (parent as? TreasureBox)?.postInvalidate()
        }
    }
}

如果要隐藏小红点,只需要将半径设置为0:

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

这套容器控件+标记控件的组合除了可以绘制小红点,还可以做其他很多事情。这是一套子控件和父控件相互通信的方式。

推荐阅读

这也是读源码长知识系列的第三篇,该系列的特点是将源码中的设计思想运用到真实项目之中,系列文章目录如下:

  1. 读源码长知识 | 更好的RecyclerView点击监听器
  2. Android自定义控件 | 源码里有宝藏之自动换行控件
  3. Android自定义控件 | 小红点的三种实现(下)
  4. 读源码长知识 | 动态扩展类并绑定生命周期的新方式
  5. 读源码长知识 | Android卡顿真的是因为”掉帧“?
目录
相关文章
|
1月前
|
缓存 搜索推荐 Android开发
安卓开发中的自定义控件实践
【10月更文挑战第4天】在安卓开发的海洋中,自定义控件是那片璀璨的星辰。它不仅让应用界面设计变得丰富多彩,还提升了用户体验。本文将带你探索自定义控件的核心概念、实现过程以及优化技巧,让你的应用在众多竞争者中脱颖而出。
|
5天前
|
前端开发 Android开发 UED
安卓应用开发中的自定义控件实践
【10月更文挑战第35天】在移动应用开发中,自定义控件是提升用户体验、增强界面表现力的重要手段。本文将通过一个安卓自定义控件的创建过程,展示如何从零开始构建一个具有交互功能的自定义视图。我们将探索关键概念和步骤,包括继承View类、处理测量与布局、绘制以及事件处理。最终,我们将实现一个简单的圆形进度条,并分析其性能优化。
|
1月前
|
缓存 搜索推荐 Android开发
安卓开发中的自定义控件基础与进阶
【10月更文挑战第5天】在Android应用开发中,自定义控件是提升用户体验和界面个性化的重要手段。本文将通过浅显易懂的语言和实例,引导你了解自定义控件的基本概念、创建流程以及高级应用技巧,帮助你在开发过程中更好地掌握自定义控件的使用和优化。
34 10
|
1月前
|
前端开发 搜索推荐 Android开发
安卓开发中的自定义控件实践
【10月更文挑战第4天】在安卓开发的世界里,自定义控件如同画家的画笔,能够绘制出独一无二的界面。通过掌握自定义控件的绘制技巧,开发者可以突破系统提供的界面元素限制,创造出既符合品牌形象又提供卓越用户体验的应用。本文将引导你了解自定义控件的核心概念,并通过一个简单的例子展示如何实现一个基本的自定义控件,让你的安卓应用在视觉和交互上与众不同。
|
2月前
|
缓存 前端开发 Android开发
安卓应用开发中的自定义控件
【9月更文挑战第28天】在安卓应用开发中,自定义控件是提升用户界面和交互体验的关键。本文通过介绍如何从零开始构建一个自定义控件,旨在帮助开发者理解并掌握自定义控件的创建过程。内容将涵盖设计思路、实现方法以及性能优化,确保开发者能够有效地集成或扩展现有控件功能,打造独特且高效的用户界面。
|
2月前
|
搜索推荐 Android开发 开发者
探索安卓开发中的自定义控件
【9月更文挑战第5天】在安卓开发的海洋中,自定义控件如同一艘精致的小船,让开发者能够乘风破浪,创造出既独特又高效的用户界面。本文将带你领略自定义控件的魅力,从基础概念到实战应用,一步步深入理解并掌握这一技术。
|
3月前
|
Android开发 UED 开发者
安卓开发中的自定义控件基础
【8月更文挑战第31天】在安卓应用开发过程中,自定义控件是提升用户界面和用户体验的重要手段。本文将通过一个简易的自定义按钮控件示例,介绍如何在安卓中创建和使用自定义控件,包括控件的绘制、事件处理以及与布局的集成。文章旨在帮助初学者理解自定义控件的基本概念,并能够动手实践,为进一步探索安卓UI开发打下坚实的基础。
|
2天前
|
搜索推荐 Android开发 开发者
探索安卓开发中的自定义视图:打造个性化UI组件
【10月更文挑战第39天】在安卓开发的世界中,自定义视图是实现独特界面设计的关键。本文将引导你理解自定义视图的概念、创建流程,以及如何通过它们增强应用的用户体验。我们将从基础出发,逐步深入,最终让你能够自信地设计和实现专属的UI组件。
|
3天前
|
Android开发 Swift iOS开发
探索安卓与iOS开发的差异和挑战
【10月更文挑战第37天】在移动应用开发的广阔舞台上,安卓和iOS这两大操作系统扮演着主角。它们各自拥有独特的特性、优势以及面临的开发挑战。本文将深入探讨这两个平台在开发过程中的主要差异,从编程语言到用户界面设计,再到市场分布的不同影响,旨在为开发者提供一个全面的视角,帮助他们更好地理解并应对在不同平台上进行应用开发时可能遇到的难题和机遇。
|
6天前
|
XML 存储 Java
探索安卓开发之旅:从新手到专家
【10月更文挑战第35天】在数字化时代,安卓应用的开发成为了一个热门话题。本文旨在通过浅显易懂的语言,带领初学者了解安卓开发的基础知识,同时为有一定经验的开发者提供进阶技巧。我们将一起探讨如何从零开始构建第一个安卓应用,并逐步深入到性能优化和高级功能的实现。无论你是编程新手还是希望提升技能的开发者,这篇文章都将为你提供有价值的指导和灵感。