日韩性视频-久久久蜜桃-www中文字幕-在线中文字幕av-亚洲欧美一区二区三区四区-撸久久-香蕉视频一区-久久无码精品丰满人妻-国产高潮av-激情福利社-日韩av网址大全-国产精品久久999-日本五十路在线-性欧美在线-久久99精品波多结衣一区-男女午夜免费视频-黑人极品ⅴideos精品欧美棵-人人妻人人澡人人爽精品欧美一区-日韩一区在线看-欧美a级在线免费观看

歡迎訪問(wèn) 生活随笔!

生活随笔

當(dāng)前位置: 首頁(yè) > 运维知识 > Android >内容正文

Android

关于 Android 中 TabLayout 下划线适配文字长度解析(附清晰详细的源码解析)

發(fā)布時(shí)間:2024/3/13 Android 41 豆豆
生活随笔 收集整理的這篇文章主要介紹了 关于 Android 中 TabLayout 下划线适配文字长度解析(附清晰详细的源码解析) 小編覺(jué)得挺不錯(cuò)的,現(xiàn)在分享給大家,幫大家做個(gè)參考.

溫故而知新 堅(jiān)持原創(chuàng) 請(qǐng)多多支持

一、問(wèn)題背景

假期在做項(xiàng)目的時(shí)候,當(dāng)時(shí)遇到了一個(gè)需求就是需要使用 TabLayout + ViewPager 來(lái)實(shí)現(xiàn)一個(gè)上部導(dǎo)航欄的動(dòng)態(tài)效果,并且希望下劃線的長(zhǎng)度等于或者小于導(dǎo)航欄中文字的寬度,當(dāng)時(shí)從網(wǎng)上查詢(xún)資料的時(shí)候是發(fā)現(xiàn)目前大概是有這么三種思路來(lái)實(shí)現(xiàn),第一種比較簡(jiǎn)單,就是直接通過(guò)自定義 CustomView 并在代碼中動(dòng)態(tài)設(shè)置給 Tab 即可,而另一種思路相對(duì)復(fù)雜一些,即利用反射的方式來(lái)進(jìn)行設(shè)置(當(dāng)時(shí)其實(shí)還不太知道可以直接通過(guò) TabLayout.setTabIndicatorFullWidth(false) 來(lái)設(shè)置下劃線寬度等于導(dǎo)航欄中文字的寬度),最后一種方法就是通過(guò)直接拷貝一份 TabLayout 代碼并修改其中的部分代碼邏輯(想讓下劃線長(zhǎng)度小于文字長(zhǎng)度好像只有這種辦法)。

但是這三種設(shè)置的方式設(shè)置出來(lái)的效果還是有挺大區(qū)別的,這個(gè)區(qū)別主要體現(xiàn)在動(dòng)畫(huà)的效果上面,即通過(guò)第一種方式設(shè)置后的下劃線雖然長(zhǎng)度雖然可以同文字相匹配,但是卻喪失了下劃線滑動(dòng)的效果,同時(shí)如果我們需要設(shè)置其長(zhǎng)度與文字的長(zhǎng)度適配等長(zhǎng),那么也意味著對(duì)于不同長(zhǎng)度的文字我們需要編寫(xiě)特定的 CustomView ,這樣的話其實(shí)工作量和代碼的重復(fù)還是挺高的,但是對(duì)于后二種解決的方案就不存在這種問(wèn)題。

因?yàn)楫?dāng)時(shí)的項(xiàng)目趕的比較急,并且當(dāng)時(shí)的 Android 方面的基礎(chǔ)也不是特別的扎實(shí),所以當(dāng)時(shí)是在簡(jiǎn)單的嘗試過(guò)第二種方式后就選擇放棄了,然后選擇了第一種較為簡(jiǎn)單的實(shí)現(xiàn)方式。后來(lái)發(fā)現(xiàn)之所以網(wǎng)上的代碼不能夠直接拿過(guò)來(lái)就是用的原因一方面是隨著 SDK 的不斷更新,不同版本的變量名和一些類(lèi)名等都已經(jīng)發(fā)生了改變,同時(shí)如果你不是真的理解了其背后的原理,那么只是照搬照抄是無(wú)濟(jì)于事的,因此當(dāng)我在做完項(xiàng)目并通過(guò)一定的技術(shù)積累之后,我又返回到了這里,希望能夠通過(guò)自己的技術(shù)積累來(lái)解決這個(gè)問(wèn)題。

因?yàn)榈谝环N方式比較簡(jiǎn)單,所以我就不在這里贅述了,直接分析第二、三種解決這個(gè)問(wèn)題的方案。同時(shí)本篇文章我將會(huì)從源碼的角度,帶領(lǐng)大家一步一步的從淺到深的去解析 TabLayout 的部分源碼,梳理 TabLayout 背后的代碼邏輯,讓大家能夠更加清晰的真正了解了為什么可以通過(guò)反射的方式來(lái)處理這個(gè)問(wèn)題。

寫(xiě)這篇文章的目的主要是有兩方面,一方面是因?yàn)楝F(xiàn)在網(wǎng)絡(luò)上有很多的類(lèi)似博文,但是都已經(jīng)過(guò)期了,即其中的很多代碼邏輯已經(jīng)不適用于當(dāng)前的 SDK 版本了,這樣的話其實(shí)反而會(huì)對(duì)初學(xué)者造成一定的誤導(dǎo)和困擾,另一方面,我覺(jué)得當(dāng)我們?cè)诮鉀Q一個(gè)問(wèn)題的時(shí)候,最重要的不是得到解決這個(gè)問(wèn)題的答案,問(wèn)題是無(wú)窮無(wú)盡的,答案也是無(wú)窮無(wú)盡的,我覺(jué)得更重要的是對(duì)于求解問(wèn)題過(guò)程中我們所能夠?qū)W習(xí)到的更多的東西,以及在這個(gè)過(guò)程中對(duì)自己的提升,因?yàn)槲乙步ㄗh大家在看完我的這篇博文之后,自己去點(diǎn)開(kāi) TabLayout 的源碼,自己去切身的走一遍解決這個(gè)問(wèn)題的邏輯,閱讀相關(guān)的代碼。

最后,在開(kāi)始分析之前,先上圖,第一幅圖是我寒假自己做的一個(gè)學(xué)生課堂狀態(tài)實(shí)時(shí)監(jiān)測(cè)系統(tǒng)的移動(dòng)端,其整個(gè)項(xiàng)目的源碼我已經(jīng)進(jìn)行了開(kāi)源,其是使用第一種方式來(lái)進(jìn)行實(shí)現(xiàn)的。第二幅圖是我后來(lái)再次自己嘗試,沒(méi)有使用?TabLayout.setTabIndicatorFullWidth(false) ,而是通過(guò)反射來(lái)達(dá)到的效果。

?

二、源碼解析

(一)結(jié)構(gòu)分析

從這里開(kāi)始我將帶大家跟隨我探索時(shí)的思路,來(lái)從源碼的角度一步一步的捋清 TabLayout 中部分代碼的邏輯。首先我們需要捋清 TabLayout 視圖的內(nèi)部結(jié)構(gòu),這樣我們才能更加清晰的閱讀后面的源碼。在這里我首先要講的就是關(guān)于 TabLayout 中的三個(gè)比較主要的類(lèi),也即 TabLayout 三大內(nèi)部結(jié)構(gòu)。

1)SlidingTabIndicator(繼承自 LinearLayout)

2)TabView(繼承自 LinearLayout)

3)Tab(靜態(tài)內(nèi)部類(lèi))

為什么說(shuō)這三個(gè)類(lèi)是最主要的類(lèi)呢,這里我們先通過(guò)源碼中的成員變量來(lái)分析一下它們各自的功能。

(1)SlidingTabIndicator

private class SlidingTabIndicator extends LinearLayout {// 下劃線的高度private int selectedIndicatorHeight;// 下劃線的畫(huà)筆private final Paint selectedIndicatorPaint;// 默認(rèn)的下劃線private final GradientDrawable defaultSelectionIndicator;int selectedPosition = -1;float selectionOffset;private int layoutDirection = -1;// 下劃線左右坐標(biāo)( 這個(gè)很關(guān)鍵 )private int indicatorLeft = -1;private int indicatorRight = -1;// 下劃線動(dòng)畫(huà)private ValueAnimator indicatorAnimator;...}

首先這個(gè)類(lèi)是繼承自 LinearLayout 的一個(gè) View,同時(shí)它是 TabLayout 的直接子 View,也就是直接位于 TabLayout 下面的子 View,當(dāng)我們閱讀源碼的時(shí)候,我們就會(huì)發(fā)現(xiàn)其實(shí) TabLayout 就是一個(gè) HorizontalScrollView ,因此其是可以進(jìn)行水平滑動(dòng)的,而其只有一個(gè)子 View 即 SlidingTabIndicator 這個(gè) LinearLayout。對(duì)于這點(diǎn),我們先不從源碼探究,先從代碼中來(lái)進(jìn)行簡(jiǎn)單的驗(yàn)證。直接編寫(xiě)上面這兩句代碼,通過(guò)運(yùn)行的結(jié)果我們可以發(fā)現(xiàn) TabLayout 確實(shí)只含有一個(gè)子 View,并且那個(gè)子 View 確實(shí)就是 SlidingTabIndicator。

Log.i("TAB", "The num of tabLayout is " + String.valueOf(mTabLayout.getChildCount())); Log.i("TAB", "The child view is " + String.valueOf(mTabLayout.getChildAt(0).getClass()));

而這個(gè)視圖或者說(shuō)這個(gè)類(lèi)的主要作用就是作為一個(gè)底部的容器,裝載著每一個(gè) Item ,這里的 Item 也就是 TabView,其最常見(jiàn)的存在方式就是一行文字或者一行圖片然后加上一個(gè)下劃線,這樣的組合就稱(chēng)為一個(gè) Item。其次通過(guò)上面的成員變量我們還可以獲取到的一個(gè)信息就是,TabView 是僅包含與圖標(biāo)和文字的,并不包含下劃線,下劃線是在 SlidingTabIndicator 中我們通過(guò)畫(huà)筆畫(huà)上去的,因此這個(gè)類(lèi)其實(shí)也就是我們下面的切入點(diǎn)。

(2)TabView

class TabView extends LinearLayout {// 數(shù)據(jù)存儲(chǔ)private TabLayout.Tab tab;// title and iconprivate TextView textView;private ImageView iconView;// CustomViewprivate View customView;private TextView customTextView;private ImageView customIconView;@Nullableprivate Drawable baseBackgroundDrawable;private int defaultMaxLines = 2;...}

這個(gè)類(lèi)也是一個(gè)繼承自 LinearLayout 的視圖,同時(shí)它就是我們上面所說(shuō)的 Item ,作為子項(xiàng)目存在于?SlidingTabIndicator 容器中。通過(guò)它的成員變量我們也可以大概了解其作用,首先我們可以發(fā)現(xiàn)它其中會(huì)保存一個(gè) Tab 實(shí)例,這個(gè)實(shí)例的作用主要就是用于存儲(chǔ)圖標(biāo)地址和標(biāo)題等信息,關(guān)于這個(gè)類(lèi)詳細(xì)講解我們放在下面。接著是一個(gè) TextView 和一個(gè) ImageView ,從變量名我們也可以猜出,它們就是導(dǎo)航欄中的標(biāo)題和圖標(biāo)。再往下的話是 CustomView ,也就是我們?cè)谏厦嫣岬降牡谝环N解決方案中自定義的 CustomView。再往下最后的就是默認(rèn)的背景圖和最大行數(shù)了,這個(gè)暫時(shí)對(duì)我們的用處不太大。

那么 TabView 是在什么時(shí)候被添加到?SlidingTabIndicator 中的呢,當(dāng)我們閱讀源碼的時(shí)候,在前半部分會(huì)發(fā)現(xiàn)下面這樣的一段代碼,我們會(huì)發(fā)現(xiàn)不管是調(diào)用哪個(gè) addTab 方法,最終都會(huì)調(diào)用 configureTab 和 addTabView 這兩個(gè)方法。

public void addTab(@NonNull TabLayout.Tab tab) {this.addTab(tab, this.tabs.isEmpty());}public void addTab(@NonNull TabLayout.Tab tab, int position) {this.addTab(tab, position, this.tabs.isEmpty());}public void addTab(@NonNull TabLayout.Tab tab, boolean setSelected) {this.addTab(tab, this.tabs.size(), setSelected);}public void addTab(@NonNull TabLayout.Tab tab, int position, boolean setSelected) {if (tab.parent != this) {throw new IllegalArgumentException("Tab belongs to a different TabLayout.");} else {this.configureTab(tab, position);this.addTabView(tab);if (setSelected) {tab.select();}}}

所以下面我們?cè)俳又鴣?lái)看這兩個(gè)方法,首先對(duì)于 configureTab 沒(méi)什么好說(shuō)的,就是將當(dāng)前的 tab 保存到 tabs 里面,便于后面的方法調(diào)用(比如外部通過(guò) getTabAt 方法來(lái)獲取指定位置的 Tab),在這個(gè)方法的后面我們看到了 addTabView 方法,我們發(fā)現(xiàn)正是再這個(gè)方法中完成了將當(dāng)前 Item 的 TabView 添加到? 中,而哪個(gè) addView 方法,就已經(jīng)是 ViewGroup 中的方法了。

private void configureTab(TabLayout.Tab tab, int position) {tab.setPosition(position);this.tabs.add(position, tab);int count = this.tabs.size();for(int i = position + 1; i < count; ++i) {((TabLayout.Tab)this.tabs.get(i)).setPosition(i);}}private void addTabView(TabLayout.Tab tab) {TabLayout.TabView tabView = tab.view;this.slidingTabIndicator.addView(tabView, tab.getPosition(), this.createLayoutParamsForTabs());}

(3)Tab

public static class Tab {public static final int INVALID_POSITION = -1;private Object tag;// 圖標(biāo)信息private Drawable icon;// 標(biāo)題信息private CharSequence text;private CharSequence contentDesc;private int position = -1;// customViewprivate View customView;// 記錄TabLayoutpublic TabLayout parent;// 綁定的 TabViewpublic TabLayout.TabView view;...}

?對(duì)于 Tab 類(lèi),正如我們上面所說(shuō)的,他主要是一個(gè)信息存儲(chǔ)的類(lèi),并且其會(huì)持有一個(gè) TabView 的引用,這樣的話其實(shí)我們就可以發(fā)現(xiàn) TabView 和 Tab 是互相持有的一個(gè)關(guān)系,TabView 主要負(fù)責(zé)視圖展示,而 Tab 主要負(fù)責(zé)信息的存儲(chǔ)和更新等,類(lèi)似于 model 和 presenter 的一個(gè)作用。

同時(shí)我們發(fā)現(xiàn)這個(gè)類(lèi)是一個(gè)靜態(tài)的內(nèi)部類(lèi),這也就說(shuō)明它可以被外部進(jìn)行引用,至于其具體的應(yīng)用如果你仔細(xì)回憶一下就會(huì)想到,當(dāng)我給 TabView 來(lái)設(shè)置視圖的時(shí)候,不管是設(shè)置 TextView 的 text 還是設(shè)置 Icon 還是設(shè)置 CustomView,其實(shí)我們都是先獲取的一個(gè) TabLayout.Tab 對(duì)象,然后來(lái)對(duì)其進(jìn)行操作,當(dāng)數(shù)據(jù)改變后就會(huì)映射到 TabView 上面。所以我們可以理解這個(gè)類(lèi)主要就是用于開(kāi)放給用戶(hù)來(lái)進(jìn)行相關(guān)的數(shù)據(jù)設(shè)置和更新視圖等。

(4)總結(jié)

所以最后總結(jié)來(lái)說(shuō),TabLayout 的結(jié)構(gòu)關(guān)系就是 TabLayout 是最外層的容器,并且是一個(gè)可以支持橫向滑動(dòng)的?HorizontalScrollView ,然后?SlidingTabIndicator 是位于 TabLayout 中的一個(gè)水平的 LinearLayout 容器,所有的 TabView 都是存在于這個(gè)容器之中的,并且我們還可以的到的一個(gè)重要信息就是下劃線是獨(dú)立于 TabView 的,最后對(duì)于 TabView 就是最核心的導(dǎo)航中的單個(gè)子項(xiàng)目,并且是一個(gè) LinearLayout ,同時(shí)其與 Tab 是一種相互持有的關(guān)系,它通過(guò) Tab 來(lái)接受外界對(duì)于視圖中數(shù)據(jù)的更新。

(二)流程分析

介紹完了 TabLayout 的內(nèi)部結(jié)構(gòu),接下來(lái)我們就來(lái)梳理在 TabLayout 中,下劃線到底是怎樣來(lái)設(shè)置的。為了便于讀者理解,所以這里我采用和我當(dāng)時(shí)分析時(shí)相同的逆推的分析方式,從后向前對(duì)其進(jìn)行推導(dǎo)。

首先我們開(kāi)始的時(shí)候如果毫無(wú)頭緒,這里我提供一個(gè)思路,就是我們可以先從外界找一個(gè)入口,比如通過(guò) TabLayout.setSelectTabIndicatorColor 這個(gè)設(shè)置下劃線顏色的方式開(kāi)始來(lái)進(jìn)入到下劃線的相關(guān)設(shè)置代碼中。這個(gè)方法的代碼很清晰,它直接調(diào)用了 slidingTabIndicator 的 setSelectedIndicator 方法,因此我們直接跟進(jìn)去。

public void setSelectedTabIndicatorColor(@ColorInt int color) {this.slidingTabIndicator.setSelectedIndicatorColor(color);}

進(jìn)到這個(gè)方法里面我們會(huì)發(fā)現(xiàn)這里就沒(méi)有什么營(yíng)養(yǎng)了,它只是簡(jiǎn)單的調(diào)用的畫(huà)筆的顏色設(shè)置方法,但是在這里我們得到了一個(gè)新的思路,也就是我們大概可以明白原來(lái)下劃線是通過(guò)這個(gè) selectedIndicatorPaint 畫(huà)筆畫(huà)出來(lái)的,這樣我們就可以去選擇跟蹤這個(gè)畫(huà)筆。?

void setSelectedIndicatorColor(int color) {if (this.selectedIndicatorPaint.getColor() != color) {this.selectedIndicatorPaint.setColor(color);ViewCompat.postInvalidateOnAnimation(this);}}

因?yàn)橥ㄟ^(guò)上面的代碼,我們基本已經(jīng)可以確定下劃線的繪制是在?selectedIndicatorPaint 中完成的了并且其是一個(gè) LinearLayout,所以我們直接從這個(gè)視圖的 onMeasure 來(lái)捋順邏輯。

對(duì)于 onMeasure 方法我們可以發(fā)現(xiàn)它大概的測(cè)量流程是這樣的,首先其通過(guò)調(diào)用父類(lèi)(LinearLayout)的 onMeasure 方法來(lái)對(duì)其子 View(TabView)來(lái)進(jìn)行遞歸測(cè)量,其次其通過(guò)遍歷求取列表中的 TabView 的最寬寬度并保存,然后它又通過(guò)一個(gè)遍歷將列表中所有的 TabView 的寬度都設(shè)定為最大值,最后進(jìn)行重繪。

其實(shí)從下面的主要流程我們已經(jīng)可以明白為什么 TabLayout 中所有的 TabView 的寬度都是相同的了,接著我們繼續(xù)往下走。?

protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {// 1.測(cè)量子視圖super.onMeasure(widthMeasureSpec, heightMeasureSpec);if (MeasureSpec.getMode(widthMeasureSpec) == 1073741824) {if (TabLayout.this.mode == 1 && TabLayout.this.tabGravity == 1) {int count = this.getChildCount();int largestTabWidth = 0;int gutter = 0;// 2.獲取列表中最大的寬度f(wàn)or(int z = count; gutter < z; ++gutter) {View child = this.getChildAt(gutter);if (child.getVisibility() == 0) {largestTabWidth = Math.max(largestTabWidth, child.getMeasuredWidth());}}if (largestTabWidth <= 0) {return;}gutter = TabLayout.this.dpToPx(16);boolean remeasure = false;if (largestTabWidth * count > this.getMeasuredWidth() - gutter * 2) {TabLayout.this.tabGravity = 0;TabLayout.this.updateTabViews(false);remeasure = true;} else {// 3.通過(guò)遍歷將所有的 TabView 的寬度都設(shè)置為最大寬度f(wàn)or(int i = 0; i < count; ++i) {android.widget.LinearLayout.LayoutParams lp = (android.widget.LinearLayout.LayoutParams)this.getChildAt(i).getLayoutParams();if (lp.width != largestTabWidth || lp.weight != 0.0F) {lp.width = largestTabWidth;lp.weight = 0.0F;remeasure = true;}}}// 4.如果求得新的最大寬度則重新測(cè)量所有 TabViewif (remeasure) {super.onMeasure(widthMeasureSpec, heightMeasureSpec);}}}}

接下來(lái)被調(diào)用的就應(yīng)該是 onLayout 布局方法,這個(gè)方法的主要作用就是對(duì)已經(jīng)測(cè)量好的視圖進(jìn)行布局放置,從源碼中我們可以看到,它依然先調(diào)用了父類(lèi)的 onLayout 方法來(lái)對(duì)其列表中的 TabView 來(lái)進(jìn)行遞歸布局放置,然后其判斷了當(dāng)前視圖是正在播放動(dòng)畫(huà),如果在播放動(dòng)畫(huà)就停止通話后再調(diào)用方法進(jìn)行布局。

這里其實(shí)我們通過(guò)上下兩個(gè)方法的名稱(chēng)已經(jīng)可以明白,其實(shí)這個(gè)當(dāng)前類(lèi)重寫(xiě)的就是對(duì)于下劃線的布局放置,并且上面的 animateIndicatorToPosition 方法就是對(duì)于下劃線動(dòng)畫(huà)的設(shè)置,而下面的 updateIndicatorPostion 方法就是對(duì)于下劃線的一個(gè)布局放置了,所以我們直接跟進(jìn)下面的方法。

protected void onLayout(boolean changed, int l, int t, int r, int b) {super.onLayout(changed, l, t, r, b);if (this.indicatorAnimator != null && this.indicatorAnimator.isRunning()) {this.indicatorAnimator.cancel();long duration = this.indicatorAnimator.getDuration();this.animateIndicatorToPosition(this.selectedPosition, Math.round((1.0F - this.indicatorAnimator.getAnimatedFraction()) * (float)duration));} else {this.updateIndicatorPosition();}}

?對(duì)于這個(gè)?updateIndicatorPosition 方法整個(gè)流程是稍微有一點(diǎn)復(fù)雜,但是我們需要看比較主要的部分,即當(dāng)不設(shè)置偏移和自定義 CustomView 的情況下,這時(shí)的話正常來(lái)說(shuō) selectedTitle 應(yīng)該是 TabView 類(lèi)型的,并且當(dāng)前 left 和 right 就應(yīng)當(dāng)是 selectedTitle 的 left 和 right 坐標(biāo),也就是 TabView 的左右坐標(biāo),也就是說(shuō)默認(rèn)情況下下劃線的寬度會(huì)等于 TabView 的寬度。最后再通過(guò)?setIndicatorPosition 方法將求得左右坐標(biāo)值賦給下劃線的左右坐標(biāo),并刷新視圖。

這里需要強(qiáng)調(diào)的是我們可以在外界通過(guò) TabLayout.setTabIndicatorFullWidth 這個(gè)方法來(lái)將?tabIndicatorFullWidth 設(shè)置為 false,這時(shí)的下劃線模式就會(huì)按照 TabView 中文字的長(zhǎng)度來(lái)設(shè)置了,所以我們接下來(lái)可以看一下這里面的?calculateTabViewContentBounds 方法的具體實(shí)現(xiàn),看看它是怎么做的。

private void updateIndicatorPosition() {// 獲取當(dāng)前被選中的子項(xiàng)目View selectedTitle = this.getChildAt(this.selectedPosition);int left;int right;if (selectedTitle != null && selectedTitle.getWidth() > 0) {// 獲取被選中子項(xiàng)目的左右坐標(biāo)left = selectedTitle.getLeft();right = selectedTitle.getRight();// 如果當(dāng)前視圖的 tabIndicatorFullWidth 被設(shè)置為 true(默認(rèn)為 true)// 同時(shí)當(dāng)前子項(xiàng)目是 TabView 類(lèi)型的if (!TabLayout.this.tabIndicatorFullWidth && selectedTitle instanceof TabLayout.TabView) {// 調(diào)用方法重新計(jì)算子項(xiàng)目的左右坐標(biāo)this.calculateTabViewContentBounds((TabLayout.TabView)selectedTitle, TabLayout.this.tabViewContentBounds);left = (int)TabLayout.this.tabViewContentBounds.left;right = (int)TabLayout.this.tabViewContentBounds.right;}// 如果設(shè)置了偏移量并且當(dāng)前所選中的子項(xiàng)目有效// 那么再次重新計(jì)算左右坐標(biāo)值if (this.selectionOffset > 0.0F && this.selectedPosition < this.getChildCount() - 1) {// 獲取下個(gè)項(xiàng)目的信息View nextTitle = this.getChildAt(this.selectedPosition + 1);int nextTitleLeft = nextTitle.getLeft();int nextTitleRight = nextTitle.getRight();// 這一步的判斷同上if (!TabLayout.this.tabIndicatorFullWidth && nextTitle instanceof TabLayout.TabView) {this.calculateTabViewContentBounds((TabLayout.TabView)nextTitle, TabLayout.this.tabViewContentBounds);nextTitleLeft = (int)TabLayout.this.tabViewContentBounds.left;nextTitleRight = (int)TabLayout.this.tabViewContentBounds.right;}// 計(jì)算左右坐標(biāo)// 公式中拋去偏移量其實(shí)也就是前一個(gè)的的坐標(biāo)加上left = (int)(this.selectionOffset * (float)nextTitleLeft + (1.0F - this.selectionOffset) * (float)left);right = (int)(this.selectionOffset * (float)nextTitleRight + (1.0F - this.selectionOffset) * (float)right);}} else {right = -1;left = -1;}this.setIndicatorPosition(left, right);}void setIndicatorPosition(int left, int right) {if (left != this.indicatorLeft || right != this.indicatorRight) {this.indicatorLeft = left;this.indicatorRight = right;ViewCompat.postInvalidateOnAnimation(this);}}

我們可以看到,這里它先通過(guò) getContentWidth 方法獲取了內(nèi)容的寬度,然后獲取內(nèi)容的中心,并設(shè)置下下劃線的左右坐標(biāo)分別為中心點(diǎn)向左向右分別增加內(nèi)容寬度的一半,這樣也就實(shí)現(xiàn)了下劃線和標(biāo)題文字寬度相等的目的。

private void calculateTabViewContentBounds(TabLayout.TabView tabView, RectF contentBounds) {int tabViewContentWidth = tabView.getContentWidth();if (tabViewContentWidth < TabLayout.this.dpToPx(24)) {tabViewContentWidth = TabLayout.this.dpToPx(24);}int tabViewCenter = (tabView.getLeft() + tabView.getRight()) / 2;int contentLeftBounds = tabViewCenter - tabViewContentWidth / 2;int contentRightBounds = tabViewCenter + tabViewContentWidth / 2;contentBounds.set((float)contentLeftBounds, 0.0F, (float)contentRightBounds, 0.0F);}private int getContentWidth() {boolean initialized = false;int left = 0;int right = 0;View[] var4 = new View[]{this.textView, this.iconView, this.customView};int var5 = var4.length;for(int var6 = 0; var6 < var5; ++var6) {View view = var4[var6];if (view != null && view.getVisibility() == 0) {left = initialized ? Math.min(left, view.getLeft()) : view.getLeft();right = initialized ? Math.max(right, view.getRight()) : view.getRight();initialized = true;}}return right - left;}

最后我們來(lái)看一下 draw 方法,在這個(gè)方法中下劃線完成了最后的繪制。對(duì)于 draw 這個(gè)方法沒(méi)有什么太多需要講的,與正常視圖的 draw 方法的區(qū)別不大,需要注意的就是它是通過(guò) Drawable.setBounds 這個(gè)方法來(lái)完成繪制的,對(duì)于這個(gè)方法作用就是繪制一個(gè)指定區(qū)域內(nèi)的矩形。這樣的話其實(shí)也就為我們提供了一個(gè)思路,那就是直接通過(guò)這個(gè)方法來(lái)設(shè)置我們想要的下劃線長(zhǎng)度。

public void draw(Canvas canvas) {int indicatorHeight = 0;if (TabLayout.this.tabSelectedIndicator != null) {indicatorHeight = TabLayout.this.tabSelectedIndicator.getIntrinsicHeight();}if (this.selectedIndicatorHeight >= 0) {indicatorHeight = this.selectedIndicatorHeight;}int indicatorTop = 0;int indicatorBottom = 0;// 下劃線模式判斷switch(TabLayout.this.tabIndicatorGravity) {case 0:// 默認(rèn)是位于 TabView 的底部的indicatorTop = this.getHeight() - indicatorHeight;indicatorBottom = this.getHeight();break;case 1:indicatorTop = (this.getHeight() - indicatorHeight) / 2;indicatorBottom = (this.getHeight() + indicatorHeight) / 2;break;case 2:indicatorTop = 0;indicatorBottom = indicatorHeight;break;case 3:indicatorTop = 0;indicatorBottom = this.getHeight();}// 如果下劃線左坐標(biāo)大于零并且右坐標(biāo)大于左坐標(biāo)// 說(shuō)明當(dāng)前的下劃線坐標(biāo)合法有效if (this.indicatorLeft >= 0 && this.indicatorRight > this.indicatorLeft) {Drawable selectedIndicator = DrawableCompat.wrap((Drawable)(TabLayout.this.tabSelectedIndicator != null ? TabLayout.this.tabSelectedIndicator : this.defaultSelectionIndicator));// 繪制下劃線selectedIndicator.setBounds(this.indicatorLeft, indicatorTop, this.indicatorRight, indicatorBottom);if (this.selectedIndicatorPaint != null) {if (VERSION.SDK_INT == 21) {selectedIndicator.setColorFilter(this.selectedIndicatorPaint.getColor(), android.graphics.PorterDuff.Mode.SRC_IN);} else {DrawableCompat.setTint(selectedIndicator, this.selectedIndicatorPaint.getColor());}}selectedIndicator.draw(canvas);}super.draw(canvas);}}

三、解決方案

對(duì)于不同的情況有不同的解決方案,如果你僅僅是需要下劃線的長(zhǎng)度等于文字的長(zhǎng)度,以前的話是只能通過(guò)反射來(lái)進(jìn)行設(shè)置,而現(xiàn)在的話可以直接通過(guò)?TabLayout.setTabIndicatorFullWidth(false) 這個(gè)方法來(lái)進(jìn)行設(shè)置,并且對(duì)于其原理我們也已經(jīng)進(jìn)行過(guò)了相應(yīng)的分析。

如果你需要下劃線的長(zhǎng)度小于文字的長(zhǎng)度的話,那么暫時(shí)還沒(méi)有太好的解決方案,這里提供一個(gè)思路,因?yàn)樵创a是不允許修改的,我們可以拷貝一份,然后對(duì)其進(jìn)行修改,然后再進(jìn)行替換。至于修改的方法其實(shí)就是我們上面最后提到的 draw 中的?Drawable.setBounds 方法,通過(guò)它來(lái)直接將我們?cè)O(shè)置的下劃線長(zhǎng)度設(shè)置給下劃線。

但是需要注意的是,我們必須要先獲取 TabView 的水平中點(diǎn),然后再?gòu)闹悬c(diǎn)分別向兩側(cè)延長(zhǎng)二分之一長(zhǎng)度的設(shè)置值,這么做是為了保證下劃線位于 TabView 的水平中心處。具體的代碼邏輯如下,直接重寫(xiě)?SlidingTabIndicator 的 draw 方法中這部分的邏輯即可(新增的代碼就是我添加注釋下面的兩行)。

if (this.indicatorLeft >= 0 && this.indicatorRight > this.indicatorLeft) {Drawable selectedIndicator = DrawableCompat.wrap((Drawable)(TabLayout.this.tabSelectedIndicator != null ? TabLayout.this.tabSelectedIndicator : this.defaultSelectionIndicator));// 求 TabView 的水平中心點(diǎn)后并繪制下劃線int indictorCenter = (this.indicatorLeft+this.indicatorRight)/2;selectedIndicator.setBounds( indictorCenter-indictorWidth/2, indicatorTop, indictorCenter+indictorWidth/2, indicatorBottom);if (this.selectedIndicatorPaint != null) {if (VERSION.SDK_INT == 21) {selectedIndicator.setColorFilter(this.selectedIndicatorPaint.getColor(), android.graphics.PorterDuff.Mode.SRC_IN);} else {DrawableCompat.setTint(selectedIndicator, this.selectedIndicatorPaint.getColor());}}selectedIndicator.draw(canvas);

最后我們還需要給外界留一個(gè)方法,使用戶(hù)可以從外界對(duì)下劃線的長(zhǎng)度進(jìn)行設(shè)置(這個(gè)方法設(shè)置在 SlidingTabIndicator 類(lèi)外部就可以了)。

int indictorWidth = 0;public int getIndictorWidth() {return indictorWidth;}public void setIndictorWidth(int indictorWidth) {this.indictorWidth = dpToPx(indictorWidth);}

?

總結(jié)

以上是生活随笔為你收集整理的关于 Android 中 TabLayout 下划线适配文字长度解析(附清晰详细的源码解析)的全部?jī)?nèi)容,希望文章能夠幫你解決所遇到的問(wèn)題。

如果覺(jué)得生活随笔網(wǎng)站內(nèi)容還不錯(cuò),歡迎將生活随笔推薦給好友。