Android自定義控件 | 小紅點的三種實現(下)

此文標題想了很久久久,本起名爲《讀原碼長知識 | 小紅點的一種實現》,但糾結了下,以爲仍是應該隸屬於自定義控件系列~~android

上篇介紹了兩種實現小紅點的方案,分別是多控件疊加和單控件繪製,其中第二個方案有一個缺點:類型綁定。致使它沒法被不一樣類型控件所複用。這篇從父控件的角度出發,提出一個新的方案:容器控件繪製,以突破類型綁定。git

這是自定義控件系列教程的第六篇,系列文章目錄以下:github

  1. Android自定義控件 | View繪製原理(畫多大?)
  2. Android自定義控件 | View繪製原理(畫在哪?)
  3. Android自定義控件 | View繪製原理(畫什麼?)
  4. Android自定義控件 | 源碼裏有寶藏之自動換行控件
  5. Android自定義控件 | 小紅點的三種實現(上)
  6. Android自定義控件 | 小紅點的三種實現(下)
  7. Anndroid自定義控件 | 小紅點的三種實現(終結)

本文使用 Kotlin 編寫,相關係列教程能夠點擊這裏canvas

引子

假設這樣一個場景:一個容器控件中,有三種不一樣類型的控件須要在右上角顯示小紅點。若使用上一篇中的「單控件繪製方案」,就必須自定義三種不一樣類型的控件,在其矩形區域的右上角繪製小紅點。數組

可不能夠把繪製工做交給容器控件?bash

容器控件能垂手可得地知道子控件矩形區域的座標,有什麼辦法把「哪些孩子須要繪製小紅點」告訴容器控件,以讓其在相應位置繪製?app

在讀androidx.constraintlayout.helper.widget.Layer源碼時,發現它用一種巧妙的方式將子控件的信息告訴容器控件。dom

Layer的啓發

綁定關聯控件

Layer是一個配合ConstraintLayout使用的控件,可實現以下效果:ide

即在不增長佈局層級的狀況下,爲一組子控件設置背景,代碼以下:

<?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)
複製代碼

這套容器控件+標記控件的組合除了能夠繪製小紅點,還能夠作其餘不少事情。這是一套子控件和父控件相互通訊的方式。

talk is cheap, show me the code

完整的源碼能夠點擊這裏

相關文章
相關標籤/搜索