自定義View和ViewGroup之TagLayout

最近幾個月終於有大把時間總結這兩年來所學 2019.6.1

前言

上一篇自己寫了一個模仿微信的SlideView,主要複習了自定義View方面的一些知識點。這幾天有看到自定義ViewGroup相關的知識點,於是也打算拿一個簡單的自定義ViewGroup來學習一下。

TagLayout

效果展示

下面這個是一個個人興趣選擇界面,裏面主要是各種各樣的Tag。下面我選取其中一部分,來實現。

我的實現

模仿的對象

使用方法

先看看怎麼用

  • 在xml中
...
<android.support.constraint.ConstraintLayout
...>

    <com.cerkerli.library.view.TagLayout
        android:id="@+id/tag_layout"
        ...>

        <com.cerkerli.library.view.TagView
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:id="@+id/material_design"
            android:text="@string/material_design"
            android:textSize="16sp"/>

        <com.cerkerli.library.view.TagView
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:id="@+id/rx_java"
            android:text="@string/rxjava"
            android:textSize="16sp"/>

        <com.cerkerli.library.view.TagView
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:id="@+id/gradle"
            android:text="@string/gradle"
            android:textSize="16sp"/>

        <com.cerkerli.library.view.TagView
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:id="@+id/android_studio"
            android:text="@string/android_studio"
            android:textSize="16sp"/>

        <com.cerkerli.library.view.TagView
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:id="@+id/kotlin"
            android:text="@string/kotlin"
            android:textSize="16sp"/>

        <com.cerkerli.library.view.TagView
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:id="@+id/flutter"
            android:text="@string/flutter"
            android:textSize="16sp"/>

        <com.cerkerli.library.view.TagView
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:id="@+id/more..."
            android:text="@string/more"
            android:textSize="16sp"/>
    </com.cerkerli.library.view.TagLayout>
</android.support.constraint.ConstraintLayout>

在activity中

        TagLayout tagLayout = findViewById(R.id.tag_layout);
        tagLayout.setOnClickListener(new Listener.TagLayoutClickListener() {
            @Override
            public void onClick(int id, String text, boolean isSelected) {
                switch (id){
                    case R.id.material_design:
                    case R.id.rx_java:
                    case R.id.gradle:
                    case R.id.android_studio:
                    case R.id.kotlin:
                    case R.id.flutter:

                        Util.toast(text + " " + isSelected);
                    break;
                    default:break;
                }
            }
        });

我們可以在activity中做響應的操作。
在Application中,做初始化。

    public void onCreate() {
        super.onCreate();
        Library.init(getApplicationContext());
    }

--------------------------------我是分割線---------------------------------


如何實現

  • 簡單介紹一下實現

TagView

TagView繼承自TextView,在TextView的基礎上添加了一個橢圓形的文本框。
重點方法有三個:onMeasure,onDraw,onTouchEvent,下面分別介紹一個每個方法。

onMeasure

onMeasure的主要功能是重新測量View的尺寸。我們知道在xml的配置中,layout_width,layout_height有三種值,分別是wrap_content,match_parent,exactly「10dp 30dp」,我在onMeasure中做了下面的處理:

    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {

        //測量文字長度
        getPaint().getTextBounds(getText().toString(), 0, getText().toString().length(), textRect);
        widthSize = textRect.right - textRect.left;
        //測量文字的推薦高度
        //這個問題被卡的比較久,之前一個用的是 textRect的top和bottom
        Paint.FontMetrics fontMetrics = getPaint().getFontMetrics();
        heightSize = (int)(fontMetrics.bottom - fontMetrics.top);
        //重新測量長和寬
        setMeasuredDimension(widthSize + heightSize * 2,
                heightSize + heightSize);
    }

這裏可以看到,我強制重新測量了文字的width和height,改成了wrap_content。這意味着,整個文本框的大小都由文字的大小決定,而不是 layout_width,layout_height 這兩個值,就是說,不管你把layout_width,layout_height寫成多少dp,最後整個文本框的大小都由 textSize參數決定。

這裏的重要知識點是文字的測量,想要具體學習的話,推薦看看Hencoder課程

onDraw

onDraw的功能比較單一了,繪製一個文本框,以及對文字做一個偏移,保證文字居中對齊於文本框。實現的效果像下面這樣子。

代碼如下:

    @Override
    protected void onDraw(Canvas canvas) {
        canvas.drawRoundRect(CIRCLE_WIDTH, CIRCLE_WIDTH,
                getWidth() - (CIRCLE_WIDTH << 1),
                getHeight() - (CIRCLE_WIDTH << 1), circleRadius, circleRadius, mPaint);
        canvas.save();
        canvas.translate(heightSize,   heightSize >> 1);
        super.onDraw(canvas);
        canvas.restore();
    }

onTouchEvent

onTouchEvent 實現點擊事件,由於這個點擊事件比較簡單,所以就沒有用框架,自己簡單實現了一下。
點擊事件重點是兩步:捕獲事件,消費事件
捕獲事件在MotionEvent.ACTION_DOWN中返回true即可。
消費事件就是在其他動作裏面做一些操作。
代碼如下:

    public boolean onTouchEvent(MotionEvent event) {
        //捕獲事件
        if(event.getActionMasked() == MotionEvent.ACTION_DOWN){
            return true;
        }
        //消費事件
        if(event.getActionMasked() == MotionEvent.ACTION_UP){
            //未點擊-->點擊
            if(!isSelected){
                isSelected = true;
                setPaintSelected(true);
                
            }else {
            //點擊-->未點擊
                isSelected = false;
                setPaintSelected(false);
            }
            //監聽回調
            if(clickListener != null){
                clickListener.onClick(getText().toString(),isSelected);
            }
            invalidate();
        }
        return false;
    }
  • TagView的部分是比較簡單的,我在實現的時候,主要是遇到了文字測量的問題,花了不少時間,其他的都很基礎。

TagLayout

TagLayout分爲兩部分,onMeasure和onLayout

onLayout

onLayout 比較容易,分別繪製每一個子View,然後將onMeasure中存儲的數據讀取出來就可以了,代碼如下:

    @Override
    protected void onLayout(boolean changed, int l, int t, int r, int b) {
        for(int i = 0;i < getChildCount();i++){
            final TagView child = (TagView)getChildAt(i);
            Rect childBond = mChildBounds.get(i);
            child.layout(childBond.left,childBond.top,childBond.right,childBond.bottom);

            //設置點擊監聽
            child.setOnClickListener(new Listener.TagViewClickListener() {
                @Override
                public void onClick(String text,boolean isSelected) {
                    if(clickListener != null){
                        clickListener.onClick(child.getId(),text,isSelected);
                    }
                }
            });
        }
    }

監聽的部分是監聽子View中的點擊事件。

onMeasure

  • onMeasure的實現非常非常麻煩,麻煩在於需要挨個測量子View的尺寸,以及計算自己的尺寸,還有各種換行的情況需要考慮,但是整體的邏輯是很簡單的,先貼一下代碼:
    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        //當前行高
        int LineWidthUsed = 0;
        int LineHeightUsed = 0;
        //總行高
        int HeightUsed = 0;
        int WidthUsed = 0;
        final int widthSize = MeasureSpec.getSize(widthMeasureSpec);
        final int widthMode = MeasureSpec.getMode(widthMeasureSpec);
        for(int i = 0;i < getChildCount();i++){
            //獲取子 View 以及佈局子 View
            View child = getChildAt(i);
            measureChildWithMargins(child,widthMeasureSpec,LineWidthUsed,heightMeasureSpec,HeightUsed);
            if(LineWidthUsed + child.getMeasuredWidth() + Constants.WIDTH_NAP > widthSize &&
                    widthMode != MeasureSpec.UNSPECIFIED){
                //是否需要換行
                WidthUsed = LineWidthUsed;
                LineWidthUsed = 0;
                HeightUsed += LineHeightUsed;
                HeightUsed += Constants.HEIGHT_NAP;
                measureChildWithMargins(child,widthMeasureSpec,LineWidthUsed,heightMeasureSpec,HeightUsed);

                LLog.d(TAG,"換行 at " + i + " WidthUsed " + WidthUsed + " HeightUsed " + HeightUsed);
            }
            //數據寫入List中 供Layout使用
            mRect = new Rect();
            mRect.set(LineWidthUsed,HeightUsed,LineWidthUsed + child.getMeasuredWidth(),HeightUsed + child.getMeasuredHeight());
            mChildBounds.add(mRect);

            LLog.d(TAG,"i == "+i);
            LLog.d(TAG,mRect);

            //數據增加
            LineWidthUsed += child.getMeasuredWidth();
            LineWidthUsed += Constants.WIDTH_NAP;
            LineHeightUsed = Math.max(LineHeightUsed,child.getMeasuredHeight());
        }
        HeightUsed+=LineHeightUsed;
        setMeasuredDimension(widthSize,  HeightUsed);

        LLog.d(TAG,"setMeasuredDimension "+ widthSize + ":" + HeightUsed);
    }

這個整體邏輯其實很簡單,就是挨個去測量每個子View的尺寸,然後記錄下來,最後把自己的總尺寸也記錄下來。
這裏有三個很關鍵的方法:measureChildWithMarginschild.getMeasuredWidth() child.getMeasuredHeight()setMeasuredDimension(...);

方法1. measureChildWithMargins:這個方法是獲取LayoutParams,然後調用child.measure(...),我可以貼一下他的源碼,

    protected void measureChildWithMargins(View child,
            int parentWidthMeasureSpec, int widthUsed,
            int parentHeightMeasureSpec, int heightUsed) {
        final MarginLayoutParams lp = (MarginLayoutParams) child.getLayoutParams();

        final int childWidthMeasureSpec = getChildMeasureSpec(parentWidthMeasureSpec,
                mPaddingLeft + mPaddingRight + lp.leftMargin + lp.rightMargin
                        + widthUsed, lp.width);
        final int childHeightMeasureSpec = getChildMeasureSpec(parentHeightMeasureSpec,
                mPaddingTop + mPaddingBottom + lp.topMargin + lp.bottomMargin
                        + heightUsed, lp.height);

        child.measure(childWidthMeasureSpec, childHeightMeasureSpec);
    }

從源碼可以看出,這個其實是一個集成好的方法,比較方便我們寫的時候,就不用在一一判斷三種值了(上面說到了wrap_content,match_parent,exactly「10dp 30dp」

方法2. child.getMeasuredWidth() child.getMeasuredHeight():這兩個是獲取子View的寬高,方便我們計算子View,然後通過子View計算自己的寬高。
方法3. setMeasuredDimension(...);:這個方法就是確定自己的寬高,和View的中用法一模一樣。

— 這裏就簡單介紹一下TagLayout的實現,需要有一些基礎才能看懂,所以沒有基礎的,建議看看Hencoder課程,這個我目前見過講自定義View最完善的課程,沒有之一。

TagLayout GitHub詳細地址

再另外,以上都是自己平時所學整理,如果有錯誤,歡迎留言或者添加微信批評指出,一起學習,共同進步,愛生活,愛技術

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章