Android 流式布局FlowLayout 實(shí)現(xiàn)關(guān)鍵字標(biāo)簽

1.介紹

流式布局的應(yīng)用還是很廣泛的辰晕,比如搜索熱詞顽分、關(guān)鍵詞標(biāo)簽等,GitHub上已經(jīng)有很多這樣的布局了丝里,但是還是想著自己實(shí)現(xiàn)一下曲初,最近一直在學(xué)自定義控件,也鞏固一下所學(xué)的知識(shí)杯聚。
本文實(shí)現(xiàn)的效果如下圖所示:

FlowLayout

2.思路

  • 繼承自RelativeLayout臼婆,可以直接使用RelativeLayout中的相關(guān)屬性,本文也可以修改為繼承ViewGroup幌绍,并不會(huì)有什么影響颁褂。
  • 在onMeasure方法中計(jì)算出所有childView的寬和高,然后根據(jù)childView的寬和高計(jì)算出布局自身的寬和高傀广。
  • 在onLayout方法中計(jì)算出所有childView的位置并進(jìn)行布局颁独。
  • 封裝Line對(duì)象,管理每行上的View對(duì)象

3.實(shí)現(xiàn)

初始化一些屬性

public class FlowLayout extends RelativeLayout {

    // 水平間距伪冰,單位為dp
    private int horizontalSpacing = dp2px(10);
    // 豎直間距誓酒,單位為dp
    private int verticalSpacing = dp2px(10);
    // 行的集合
    private List<Line> lines = new ArrayList<Line>();
    // 當(dāng)前的行
    private Line line;
    // 當(dāng)前行使用的空間
    private int lineSize = 0;
    // 關(guān)鍵字大小,單位為sp
    private int textSize = sp2px(15);
    // 關(guān)鍵字顏色
    private int textColor = Color.BLACK;
    // 關(guān)鍵字背景框
    private int backgroundResource = R.drawable.bg_frame;
    // 關(guān)鍵字水平padding贮聂,單位為dp
    private int textPaddingH = dp2px(7);
    // 關(guān)鍵字豎直padding靠柑,單位為dp
    private int textPaddingV = dp2px(4);

    public FlowLayout(Context context) {
        this(context, null);
    }

    public FlowLayout(Context context, AttributeSet attrs) {
        this(context, attrs, 0);
    }

    public FlowLayout(Context context, AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
        TypedArray typedArray = context.getTheme().obtainStyledAttributes(
                attrs, R.styleable.FlowLayoutAttrs, defStyleAttr, 0);

        int count = typedArray.getIndexCount();
        for (int i = 0; i < count; i++) {
            int attr = typedArray.getIndex(i);
            switch (attr) {
                case R.styleable.FlowLayoutAttrs_horizontalSpacing:
                    horizontalSpacing = typedArray.getDimensionPixelSize(attr, dp2px(10));
                    break;

                case R.styleable.FlowLayoutAttrs_verticalSpacing:
                    verticalSpacing = typedArray.getDimensionPixelSize(attr, dp2px(10));
                    break;

                case R.styleable.FlowLayoutAttrs_textSize:
                    textSize = typedArray.getDimensionPixelSize(attr, sp2px(15));
                    break;

                case R.styleable.FlowLayoutAttrs_textColor:
                    textColor = typedArray.getColor(attr, Color.BLACK);
                    break;

                case R.styleable.FlowLayoutAttrs_backgroundResource:
                    backgroundResource = typedArray.getResourceId(attr, R.drawable.bg_frame);
                    break;

                case R.styleable.FlowLayoutAttrs_textPaddingH:
                    textPaddingV = typedArray.getDimensionPixelSize(attr, dp2px(7));
                    break;

                case R.styleable.FlowLayoutAttrs_textPaddingV:
                    verticalSpacing = typedArray.getDimensionPixelSize(attr, dp2px(4));
                    break;
            }
        }
        typedArray.recycle();
    }
    
    ...
}

onMeasure

首先獲取父容器傳入的寬高值與測(cè)量模式,計(jì)算出實(shí)際使用的寬和高吓懈,遍歷所有的childView歼冰,對(duì)childView進(jìn)行測(cè)量,根據(jù)當(dāng)前行已用的寬度判斷是否需要換行耻警,然后累計(jì)所有高度隔嫡,設(shè)置布局自身尺寸甸怕。

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

    // 實(shí)際可以用的寬和高
    int width = MeasureSpec.getSize(widthMeasureSpec) - getPaddingLeft() - getPaddingRight();
    int height = MeasureSpec.getSize(heightMeasureSpec) - getPaddingBottom() - getPaddingTop();
    int widthMode = MeasureSpec.getMode(widthMeasureSpec);
    int heightMode = MeasureSpec.getMode(heightMeasureSpec);

    // Line初始化
    restoreLine();

    for (int i = 0; i < getChildCount(); i++) {
        View child = getChildAt(i);
        // 測(cè)量所有的childView
        int childWidthMeasureSpec = MeasureSpec.makeMeasureSpec(width,
                widthMode == MeasureSpec.EXACTLY ? MeasureSpec.AT_MOST : widthMode);
        int childHeightMeasureSpec = MeasureSpec.makeMeasureSpec(height,
                heightMode == MeasureSpec.EXACTLY ? MeasureSpec.AT_MOST : heightMode);
        child.measure(childWidthMeasureSpec, childHeightMeasureSpec);

        if (line == null) {
            // 創(chuàng)建新一行
            line = new Line();
        }

        // 計(jì)算當(dāng)前行已使用的寬度
        int measuredWidth = child.getMeasuredWidth();
        lineSize += measuredWidth;

        // 如果使用的寬度小于可用的寬度,這時(shí)候childView能夠添加到當(dāng)前的行上
        if (lineSize <= width) {
            line.addChild(child);
            lineSize += horizontalSpacing;
        } else {
            // 換行
            newLine();
            line.addChild(child);
            lineSize += child.getMeasuredWidth();
            lineSize += horizontalSpacing;
        }
    }

    // 把最后一行記錄到集合中
    if (line != null && !lines.contains(line)) {
        lines.add(line);
    }

    int totalHeight = 0;
    // 把所有行的高度加上
    for (int i = 0; i < lines.size(); i++) {
        totalHeight += lines.get(i).getHeight();
    }
    // 加上行的豎直間距
    totalHeight += verticalSpacing * (lines.size() - 1);
    // 加上上下padding
    totalHeight += getPaddingBottom();
    totalHeight += getPaddingTop();

    // 設(shè)置自身尺寸
    // 設(shè)置布局的寬高畔勤,寬度直接采用父view傳遞過來的最大寬度蕾各,而不用考慮子view是否填滿寬度
    // 因?yàn)樵摬季值奶匦跃褪翘顫M一行后,再換行
    // 高度根據(jù)設(shè)置的模式來決定采用所有子View的高度之和還是采用父view傳遞過來的高度
    setMeasuredDimension(MeasureSpec.getSize(widthMeasureSpec),
            resolveSize(totalHeight, heightMeasureSpec));
}

附上restoreLine與newLine方法

private void restoreLine() {
    lines.clear();
    line = new Line();
    lineSize = 0;
}

private void newLine() {
    // 把之前的行記錄下來
    if (line != null) {
        lines.add(line);
    }
    // 創(chuàng)建新的一行
    line = new Line();
    lineSize = 0;
}

Line

封裝了每行上的View對(duì)象庆揪,提供添加與繪制childView的方法式曲。

/**
 * 管理每行上的View對(duì)象
 */
class Line {
    // 子控件集合
    private List<View> children = new ArrayList<View>();
    // 行高
    int height;

    /**
     * 添加childView
     *
     * @param childView 子控件
     */
    public void addChild(View childView) {
        children.add(childView);

        // 讓當(dāng)前的行高是最高的一個(gè)childView的高度
        if (height < childView.getMeasuredHeight()) {
            height = childView.getMeasuredHeight();
        }
    }

    /**
     * 設(shè)置childView的繪制區(qū)域
     *
     * @param left 左上角x軸坐標(biāo)
     * @param top  左上角y軸坐標(biāo)
     */
    public void layout(int left, int top) {
        int totalWidth = getMeasuredWidth() - getPaddingLeft() - getPaddingRight();
        // 當(dāng)前childView的左上角x軸坐標(biāo)
        int currentLeft = left;

        for (int i = 0; i < children.size(); i++) {
            View view = children.get(i);
            // 設(shè)置childView的繪制區(qū)域
            view.layout(currentLeft, top, currentLeft + view.getMeasuredWidth(),
                    top + view.getMeasuredHeight());
            // 計(jì)算下一個(gè)childView的位置
            currentLeft = currentLeft + view.getMeasuredWidth() + horizontalSpacing;
        }
    }

    public int getHeight() {
        return height;
    }

    public int getChildCount() {
        return children.size();
    }
}

onLayout

指定所有childView的位置,調(diào)用Line對(duì)象中的layout方法缸榛。

@Override
protected void onLayout(boolean changed, int l, int t, int r, int b) {
    super.onLayout(changed, l, t, r, b);
    int left = getPaddingLeft();
    int top = getPaddingTop();
    for (int i = 0; i < lines.size(); i++) {
        Line line = lines.get(i);
        line.layout(left, top);
        // 計(jì)算下一行的起點(diǎn)y軸坐標(biāo)
        top = top + line.getHeight() + verticalSpacing;
    }
}

setFlowLayout

用于設(shè)置FlowLayout中的內(nèi)容吝羞,并提供點(diǎn)擊事件處理。

public void setFlowLayout(List<String> list, final OnItemClickListener onItemClickListener) {
    for (int i = 0; i < list.size(); i++) {
        final TextView tv = new TextView(getContext());

        // 設(shè)置TextView屬性
        tv.setText(list.get(i));
        tv.setTextSize(TypedValue.COMPLEX_UNIT_PX, textSize);
        tv.setTextColor(textColor);
        tv.setGravity(Gravity.CENTER);
        tv.setPadding(textPaddingH, textPaddingV, textPaddingH, textPaddingV);

        tv.setClickable(true);
        tv.setBackgroundResource(backgroundResource);
        this.addView(tv, new ViewGroup.LayoutParams(ViewGroup.LayoutParams.WRAP_CONTENT,
                ViewGroup.LayoutParams.WRAP_CONTENT));

        tv.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                onItemClickListener.onItemClick(tv.getText().toString());
            }
        });
    }
}

public interface OnItemClickListener {
    void onItemClick(String content);
}

使用方法

  • 在項(xiàng)目根目錄的build.gradle文件中加入如下代碼
maven { url "https://jitpack.io" }
jitpack
  • 在app根目錄的buil.gradle文件中加入依賴
compile 'com.github.alidili:FlowLayout:v1.0'
加入依賴
  • 在Activity中使用内颗,設(shè)置點(diǎn)擊事件

相關(guān)屬性(字體大小钧排、顏色、間距等)可以在布局文件中設(shè)置均澳,也可以通過set方法設(shè)置恨溜。

public class MainActivity extends AppCompatActivity {

    private FlowLayout flKeyword;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        flKeyword = (FlowLayout) findViewById(R.id.fl_keyword);

        List<String> list = new ArrayList<>();
        list.add("關(guān)鍵詞一");
        list.add("關(guān)鍵詞二");
        list.add("關(guān)鍵詞三");
        list.add("關(guān)鍵詞四");
        list.add("關(guān)鍵詞五");
        flKeyword.setFlowLayout(list, new FlowLayout.OnItemClickListener() {
            @Override
            public void onItemClick(String content) {
                Toast.makeText(MainActivity.this, content, Toast.LENGTH_SHORT).show();
            }
        });
    }
}
  • 布局文件
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout 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">

    <com.yang.flowlayoutlibrary.FlowLayout
        android:id="@+id/fl_keyword"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:layout_marginLeft="10dp"
        android:layout_marginRight="10dp"
        android:layout_marginTop="10dp"
        app:itemColor="@color/colorAccent"
        app:itemSize="15sp" />

</RelativeLayout>

4.寫在最后

源碼已托管到GitHub上,歡迎Fork找前,覺得還不錯(cuò)就Start一下吧糟袁!

點(diǎn)擊下載源碼

GitHub地址:https://github.com/alidili/FlowLayout

歡迎同學(xué)們吐槽評(píng)論,如果你覺得本篇博客對(duì)你有用躺盛,那么就留個(gè)言或者點(diǎn)下喜歡吧(^-^)

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末项戴,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子槽惫,更是在濱河造成了極大的恐慌周叮,老刑警劉巖,帶你破解...
    沈念sama閱讀 212,383評(píng)論 6 493
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件界斜,死亡現(xiàn)場(chǎng)離奇詭異仿耽,居然都是意外死亡,警方通過查閱死者的電腦和手機(jī)各薇,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 90,522評(píng)論 3 385
  • 文/潘曉璐 我一進(jìn)店門氓仲,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人得糜,你說我怎么就攤上這事∥鳎” “怎么了朝抖?”我有些...
    開封第一講書人閱讀 157,852評(píng)論 0 348
  • 文/不壞的土叔 我叫張陵,是天一觀的道長谍珊。 經(jīng)常有香客問我治宣,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 56,621評(píng)論 1 284
  • 正文 為了忘掉前任侮邀,我火速辦了婚禮坏怪,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘绊茧。我一直安慰自己铝宵,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 65,741評(píng)論 6 386
  • 文/花漫 我一把揭開白布华畏。 她就那樣靜靜地躺著鹏秋,像睡著了一般。 火紅的嫁衣襯著肌膚如雪亡笑。 梳的紋絲不亂的頭發(fā)上侣夷,一...
    開封第一講書人閱讀 49,929評(píng)論 1 290
  • 那天,我揣著相機(jī)與錄音仑乌,去河邊找鬼百拓。 笑死,一個(gè)胖子當(dāng)著我的面吹牛晰甚,可吹牛的內(nèi)容都是我干的衙传。 我是一名探鬼主播,決...
    沈念sama閱讀 39,076評(píng)論 3 410
  • 文/蒼蘭香墨 我猛地睜開眼压汪,長吁一口氣:“原來是場(chǎng)噩夢(mèng)啊……” “哼粪牲!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起止剖,我...
    開封第一講書人閱讀 37,803評(píng)論 0 268
  • 序言:老撾萬榮一對(duì)情侶失蹤腺阳,失蹤者是張志新(化名)和其女友劉穎,沒想到半個(gè)月后穿香,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體亭引,經(jīng)...
    沈念sama閱讀 44,265評(píng)論 1 303
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 36,582評(píng)論 2 327
  • 正文 我和宋清朗相戀三年皮获,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了焙蚓。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 38,716評(píng)論 1 341
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡洒宝,死狀恐怖购公,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情雁歌,我是刑警寧澤宏浩,帶...
    沈念sama閱讀 34,395評(píng)論 4 333
  • 正文 年R本政府宣布,位于F島的核電站靠瞎,受9級(jí)特大地震影響比庄,放射性物質(zhì)發(fā)生泄漏求妹。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 40,039評(píng)論 3 316
  • 文/蒙蒙 一佳窑、第九天 我趴在偏房一處隱蔽的房頂上張望制恍。 院中可真熱鬧,春花似錦神凑、人聲如沸净神。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,798評(píng)論 0 21
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽强挫。三九已至,卻和暖如春薛躬,著一層夾襖步出監(jiān)牢的瞬間俯渤,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 32,027評(píng)論 1 266
  • 我被黑心中介騙來泰國打工型宝, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留八匠,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 46,488評(píng)論 2 361
  • 正文 我出身青樓趴酣,卻偏偏與公主長得像梨树,于是被迫代替她去往敵國和親。 傳聞我的和親對(duì)象是個(gè)殘疾皇子岖寞,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 43,612評(píng)論 2 350

推薦閱讀更多精彩內(nèi)容