一行代碼實現底部導航欄TabLayout

歡迎關注公眾號:JueCode

app中底部導航欄已經是很常見的控件了,比如微信,簡書,QQ等都有這類控件,都是點擊底部標簽切換界面。主要的實現手段有

  • RadioGroup
  • FragmentTabLayout
  • TabLayout
  • Bottom Navigation

其中TabLayout一般作為頂部的導航欄使用,今天我們基于FragmentTabLayout來實現一個底部導航欄。先看下實現的效果:


今天這個探索會按照下面這個步驟:

  • FrameTabLayout布局
  • 自定義控件
  • 接口封裝
  • 一行代碼使用
  • FrameTabLayout源碼分析

好了,準備開車~~~

1.FrameTabLayout布局

為什么要提下這個布局,其實這個系統自帶的布局比較特殊,要使用系統的id,也就是我們不能自己命名android:id,我們對著具體的布局實現R.layout.myfragment_tab_layout看比較容易明白。 布局其實比較簡單,有幾個點需要注意下的

id是android:id/tabcontent的FrameLayout明顯就是放置內容的,我們的栗子中就是放置Fragment,這個id就是用的系統的不能做更改

id是android:id/tabs的TabWidget顧名思義就是放置底部標簽的,就是上圖中的Home,Contact等等balabala,對的,你猜到了,這個id也是不能改

為了區分,我故意用了兩種高調的顏色作為區分,上圖中綠色的區域就是FrameLayout, 橙色的區域就是TabWidget

<android.support.v4.app.FragmentTabHost xmlns:android="http://schemas.android.com/apk/res/android"xmlns:tools="http://schemas.android.com/tools"android:id="@android:id/tabhost"android:layout_width="match_parent"android:layout_height="match_parent"android:orientation="vertical"tools:context="com.example.juexingzhe.testfragmenttablayout.MainActivity"><LinearLayoutandroid:layout_width="match_parent"android:layout_height="match_parent"android:orientation="vertical"><FrameLayoutandroid:id="@android:id/tabcontent"android:layout_width="match_parent"android:layout_height="0dp"android:layout_weight="1"android:background="@android:color/holo_green_dark" /><TabWidgetandroid:id="@android:id/tabs"android:layout_width="match_parent"android:layout_height="?attr/actionBarSize"android:layout_gravity="bottom"android:background="@android:color/holo_orange_dark" /></LinearLayout>
</android.support.v4.app.FragmentTabHost>
復制代碼

具體為什么id不能改,后面我們分析源碼的時候就知道了,先按下,客官繼續往后看~~~

2.自定義控件MyFragmentTabLayout

這里為了方便我們直接繼承自FragmentTabHost,也沒有自定義屬性(請原諒我偷懶),上來就是加載上面貼出來的布局, dividerDrawable就是用來設置底部標簽欄標簽之間分割線用途。

private void init(){View view = LayoutInflater.from(getContext()).inflate(R.layout.myfragment_tab_layout, this, true);fragmentTabHost = (FragmentTabHost) view.findViewById(android.R.id.tabhost);dividerDrawable = null;
}
復制代碼

在繼續往下說之前,我們先看下如果不自定義這個控件,我們是怎么使用FragmentTabHost的,我下面貼出的是示意代碼,不能直接使用的,不過也可以看出來比較繁瑣,也直接證明了封裝的必要性。

fragmentTabHost.setup(getContext(), fragmentManager, android.R.id.tabcontent);
TabSpec tabSpec = fragmentTabHost.newTabSpec(……);
fragmentTabHost.addTab(tabSpec, fragment.class, bundle);
fragmentTabHost.getTabWidget().setDividerDrawable(……);
復制代碼

我們對著上面的示意過程來接著看下自定義MyFragmentTabLayout控件剩下的過程。這個方法其實就是調用setup,方法的原型是setup(Context context, FragmentManager manager, int containerId)第一個context沒什么好說的,需要外界傳入fragmentManager,用來管理fragment,containerId就是用來放置內容的控件id,就是我們上面綠色背景的FrameLayout。

public MyFragmentTabLayout init(FragmentManager fragmentManager) {fragmentTabHost.setup(getContext(), fragmentManager, android.R.id.tabcontent);return this;
}
復制代碼

經過上面的過程fragmentTabHost的初始化過程就結束了。有些小伙伴就急了,底部標簽欄還沒見蹤影呢???別急,聽我娓娓道來(逃),底部標簽欄的個數肯定是不能寫死的,最好是根據數據的數量來做決定,google就是這么做的,因此標簽的初始化是要在fragmentTabHost的數據初始化過程中進行。具體實現代碼往下看。

  • fragmentTabHost.newTabSpec這個方法就是用來構造底部標簽欄,需要傳入一個Tag,和一個tabview,我們這里很簡單就是上面圖片下面文字的布局
  • fragmentTabHost.addTab就是構造內容區域(fragment)和底部標簽欄,有需要傳遞給fragment的數據可以通過bundle傳送
  • setDividerDrawable我們這里傳入null,就是不需要分割線,默認是有分割線:

  • setOnTabChangedListener就是設置標簽的點擊事件
public MyFragmentTabLayout creat(){if (fragmentTabLayoutAdapter == null) return null;TabInfo tabInfo;for (int i = 0; i < fragmentTabLayoutAdapter.getCount(); i++){tabInfo = fragmentTabLayoutAdapter.getTabInfo(i);TabSpec tabSpec = fragmentTabHost.newTabSpec(tabInfo.getTabTag()).setIndicator(tabInfo.getTabView());fragmentTabHost.addTab(tabSpec, tabInfo.getFragmentClass(), tabInfo.getBundle());fragmentTabHost.getTabWidget().setDividerDrawable(dividerDrawable);fragmentTabHost.setOnTabChangedListener(new OnTabChangeListener() {@Overridepublic void onTabChanged(String tabId) {int currentTab = fragmentTabHost.getCurrentTab();fragmentTabLayoutAdapter.onClick(currentTab);}});}return this;
}
復制代碼

底部標簽布局:

<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"android:layout_width="match_parent"android:layout_height="match_parent"android:orientation="vertical"android:gravity="center"><ImageViewandroid:id="@+id/img"android:layout_width="match_parent"android:layout_height="wrap_content" /><TextViewandroid:gravity="center"android:id="@+id/tab_text"android:layout_width="match_parent"android:layout_height="wrap_content" />
</LinearLayout>
復制代碼

上面代碼是經過接口封裝的,我們接著往下看

3.接口封裝

我們也是在控件中留出來一個接口做hook,用戶可以通過接口給控件定制數據,定制標簽布局,定制點擊事件

public interface FragmentTabLayoutAdapter{int getCount();TabInfo getTabInfo(int pos);View createView(int pos);void onClick(int pos);}
復制代碼

我們再回顧下上面自定義的過程,標簽的個數通過getCount得到;構造每個標簽需要的數據都從getTabInfo獲得,參數pos就是標簽的位置;每個標簽的布局則通過createView獲得,參數pos同上;onClick就是標簽的點擊事件,參數pos同上。

4.一行代碼使用

到這里自定義導航欄的工作就差不多了,我們看下具體怎么用,首先就是在布局文件中聲明控件,這個布局文件很簡單就是引用我們自定義的控件,沒什么好解釋的。

<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"xmlns:tools="http://schemas.android.com/tools"android:layout_width="match_parent"android:layout_height="match_parent"tools:context="com.example.juexingzhe.testfragmenttablayout.MainActivity"android:orientation="vertical"><com.example.juexingzhe.testfragmenttablayout.MyFragmentTabLayoutandroid:id="@+id/tab_layout"android:layout_width="match_parent"android:layout_height="match_parent"/></LinearLayout>
復制代碼

接下來在代碼中用一行代碼實現即可,傳入fragmentManager進行初始化,然后就是傳入接口FragmentTabLayoutAdapter的實現,我們這里也進行了抽取,提供一個默認的實現,用戶只需要實現createView 定制自己需要顯示的布局和實現onClick定制每個標簽的點擊事件,我們這里為了簡化只是通過一個Toast進行演示。

fragmentTabHost.init(getSupportFragmentManager()).setFragmentTabLayoutAdapter(new DefaultFragmentTabAdapter(Arrays.asList(fragmentClass), Arrays.asList(textViewArray), Arrays.asList(drawables)){@Overridepublic View createView(int pos) {View view = LayoutInflater.from(MainActivity.this).inflate(R.layout.tab_item, null);ImageView imageView = (ImageView) view.findViewById(R.id.img);imageView.setImageResource(drawables[pos]);TextView textView = (TextView) view.findViewById(R.id.tab_text);textView.setText(textViewArray[pos]);return view;}@Overridepublic void onClick(int pos) {Toast.makeText(MainActivity.this, textViewArray[pos] + " be clicked", Toast.LENGTH_SHORT).show();}}).creat();
復制代碼

是不是說話算話,一行代碼搞定。我們看下DefaultFragmentTabAdapter的實現,默認實現了兩個方法getCount和getTabInfo,第一個方法地球人都知道,第二個方法就是構造每個標簽需要數據信息。

public class DefaultFragmentTabAdapter implements MyFragmentTabLayout.FragmentTabLayoutAdapter {private List<Class> fragmentclass = new ArrayList<>();private List<String> fragmentTag = new ArrayList<>();private List<Integer> drawables = new ArrayList<>();public DefaultFragmentTabAdapter(List<Class> fragmentclass, List<String> fragmentTag, List<Integer> drawables) {this.fragmentclass = fragmentclass;this.fragmentTag = fragmentTag;this.drawables = drawables;}@Overridepublic int getCount() {return fragmentTag.size();}@Overridepublic TabInfo getTabInfo(int pos) {return new TabInfo.Builder(fragmentTag.get(pos), createView(pos), fragmentclass.get(pos)).build();}@Overridepublic View createView(int pos) {return null;}@Overridepublic void onClick(int pos) {}
}
復制代碼

稍微提下TabInfo這個數據類,從上面可以看出也是build模式,這里就不多做介紹。幾個屬性,tabTag就是TabSpec需要傳入的Tag;tabView就是底部標簽的布局;fragmentClass就是每個標簽對應的fragment;bundle是fragment對應的數據;backgroundRes就是每個標簽的背景,可以設置點擊時的背景變化。

public class TabInfo {String tabTag;View tabView;Class fragmentClass;Bundle bundle;int backgroundRes;……
}
復制代碼

5.FrameTabLayout源碼分析

我們接著簡單看下FrameTabLayout的源碼,首先就是初始化時見到的setup方法,主要工作在ensureHierarchy方法中,我們接著跟。

public void setup(Context context, FragmentManager manager, int containerId) {ensureHierarchy(context);  // Ensure views required by super.setup()super.setup();mContext = context;mFragmentManager = manager;mContainerId = containerId;ensureContent();mRealTabContent.setId(containerId);// We must have an ID to be able to save/restore our state.  If// the owner hasn't set one at this point, we will set it ourselves.if (getId() == View.NO_ID) {setId(android.R.id.tabhost);}
}
復制代碼

這個方法是跟布局比較密切相關的,也能解釋我們前面說的布局id寫死的問題。如果沒有找到id是android.R.id.tabs的TabWidget,系統會為我們生成一個布局,其中TabWidget就是底部標簽欄,id是android.R.id.tabs和我們上面布局代碼中一樣的;mRealTabContent就是放置內容區域,是一個FrameLayout布局,id是 android.R.id.tabcontent,和我們上面布局代碼FrameLayout是一樣的。

private void ensureHierarchy(Context context) {// If owner hasn't made its own view hierarchy, then as a convenience// we will construct a standard one here.if (findViewById(android.R.id.tabs) == null) {LinearLayout ll = new LinearLayout(context);ll.setOrientation(LinearLayout.VERTICAL);addView(ll, new FrameLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT,ViewGroup.LayoutParams.MATCH_PARENT));TabWidget tw = new TabWidget(context);tw.setId(android.R.id.tabs);tw.setOrientation(TabWidget.HORIZONTAL);ll.addView(tw, new LinearLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT,ViewGroup.LayoutParams.WRAP_CONTENT, 0));FrameLayout fl = new FrameLayout(context);fl.setId(android.R.id.tabcontent);ll.addView(fl, new LinearLayout.LayoutParams(0, 0, 0));mRealTabContent = fl = new FrameLayout(context);mRealTabContent.setId(mContainerId);ll.addView(fl, new LinearLayout.LayoutParams(LinearLayout.LayoutParams.MATCH_PARENT, 0, 1));}}
復制代碼

我們往下看addTab方法,這個方法就是綁定布局和數據。根據傳入的TabSpec構造TabInfo,然后調用TabHost中的addTab(tabSepc) 方法。

public void addTab(@NonNull TabHost.TabSpec tabSpec, @NonNull Class<?> clss,@Nullable Bundle args) {tabSpec.setContent(new DummyTabFactory(mContext));final String tag = tabSpec.getTag();final TabInfo info = new TabInfo(tag, clss, args);if (mAttached) {// If we are already attached to the window, then check to make// sure this tab's fragment is inactive if it exists.  This shouldn't// normally happen.info.fragment = mFragmentManager.findFragmentByTag(tag);if (info.fragment != null && !info.fragment.isDetached()) {final FragmentTransaction ft = mFragmentManager.beginTransaction();ft.detach(info.fragment);ft.commit();}}mTabs.add(info);addTab(tabSpec);
}
復制代碼

在addTab(tabSepc) 方法中mTabWidget.addView(tabIndicator)就是添加底部標簽,那么Fragment呢?猜下應該是在setCurrentTab(0)進行添加,我們往下看。

public void addTab(TabSpec tabSpec) {……mTabWidget.addView(tabIndicator);mTabSpecs.add(tabSpec);if (mCurrentTab == -1) {setCurrentTab(0);}}
復制代碼

在setCurrentTab方法中會調用invokeOnTabChangeListener()方法,最后調用onTabChanged方法,FragmentTabHost是實現了OnTabChangeListener接口,我們再回到FragmentTabHost往下看

private void invokeOnTabChangeListener() {if (mOnTabChangeListener != null) {mOnTabChangeListener.onTabChanged(getCurrentTabTag());}
}/*** Interface definition for a callback to be invoked when tab changed*/
public interface OnTabChangeListener {void onTabChanged(String tabId);
}
復制代碼

先調用doTabChanged,然后會處理我們定義的點擊事件,我們往下看doTabChanged方法。如果存在fragment就直接attach,否則先Fragment.instantiate構造Fragment,然后通過add方法進行添加。看到這里整個流程也就清楚了。

public void onTabChanged(String tabId) {if (mAttached) {final FragmentTransaction ft = doTabChanged(tabId, null);if (ft != null) {ft.commit();}}if (mOnTabChangeListener != null) {mOnTabChangeListener.onTabChanged(tabId);}
}private FragmentTransaction doTabChanged(@Nullable String tag,@Nullable FragmentTransaction ft) {final TabInfo newTab = getTabInfoForTag(tag);if (mLastTab != newTab) {if (ft == null) {ft = mFragmentManager.beginTransaction();}if (mLastTab != null) {if (mLastTab.fragment != null) {ft.detach(mLastTab.fragment);}}if (newTab != null) {if (newTab.fragment == null) {newTab.fragment = Fragment.instantiate(mContext,newTab.clss.getName(), newTab.args);ft.add(mContainerId, newTab.fragment, newTab.tag);} else {ft.attach(newTab.fragment);}}mLastTab = newTab;
}      
復制代碼

6.總結

如果你能看到這里,說明是真愛。使用FragmentTabHost需要注意的就是布局的時候幾個id的問題,更簡單的辦法就是使用我封裝的控件,就沒什么需要注意的了:)

代碼放到網上,有需要的自行下載,別忘了點贊哦。 GitHub地址

今天的自定義FragmentTabLayout之旅就到這里結束了,大家可以下車了,你們的贊是我最大的動力,謝謝!

本文來自互聯網用戶投稿,該文觀點僅代表作者本人,不代表本站立場。本站僅提供信息存儲空間服務,不擁有所有權,不承擔相關法律責任。
如若轉載,請注明出處:http://www.pswp.cn/news/278139.shtml
繁體地址,請注明出處:http://hk.pswp.cn/news/278139.shtml
英文地址,請注明出處:http://en.pswp.cn/news/278139.shtml

如若內容造成侵權/違法違規/事實不符,請聯系多彩編程網進行投訴反饋email:809451989@qq.com,一經查實,立即刪除!

相關文章

小程序視頻截gif_3個簡單的應用程序,可讓您深入視頻和GIF

小程序視頻截gifDeepfakes make it possible to manipulate videos and GIFs. The technology has become so easy to use, you can now create deepfakes right on your phone. That’s right—you can now easily insert yourself into a meme. 借助Deepfake &#xff0c;可以…

【AtCoder】ARC095 E - Symmetric Grid 模擬

【題目】E - Symmetric Grid 【題意】給定n*m的小寫字母矩陣&#xff0c;求是否能通過若干行互換和列互換使得矩陣中心對稱。n,m<12。 【算法】模擬 【題解】首先行列操作獨立&#xff0c;如果已確定行操作&#xff0c;那么兩個在對稱位置的列要滿足條件必須其中一列反轉后和…

一、內存尋址

1.內存地址分類: 邏輯地址、線性地址、物理地址 邏輯地址:段選擇符偏移量 線性地址:C語言中取地址符&打印出來的地址就是這個地址&#xff0c;也叫虛擬地址。 物理地址:內存總線尋址的具體地址&#xff0c;是真實存在的。 邏輯地址通過分段單元轉換成線性地址&#xff0c;線…

如何使用Google TV設置Chromecast

Justin Duino賈斯汀杜伊諾(Justin Duino)Google changed up its streaming platform with the release of the Chromecast with Google TV. Instead of being a Cast-only device like Chromecasts before it, Google’s latest dongle runs the successor of Android TV. If y…

js之 foreach, map, every, some

js中array有四個方法 foreach, map, every, some&#xff0c;其使用各有傾向。 關注點一&#xff1a;foreach 和 map 無法跳出循環&#xff0c;每個元素均執行foreach 和 map 無法跳出循環&#xff0c;他們是對每個數組元素調用 callback&#xff1b; foreach 無返回值&#xf…

scala 方法、函數定義小結

2019獨角獸企業重金招聘Python工程師標準>>> package scalapackage.testmethod/*** Created by Germmy on 2018/4/15.*/ object TesMethod {def main(args: Array[String]) {//定義方法的一種方法,高階函數的一種定義方法def m1(x:Int)(y:Int)x*yval resm1(3)(4)pri…

ipad和iphone切圖_如何在iPhone和iPad上密碼保護照片

ipad和iphone切圖Sometimes, you need to protect your iPhone or iPad photos from prying eyes that might also have access to your device. Unfortunately, Apple doesn’t provide an obvious, secure way to do this. However, there’s a work-around thanks to the No…

Java高級篇(二)——網絡通信

網絡編程是每個開發人員工具箱中的核心部分&#xff0c;我們在學習了諸多Java的知識后&#xff0c;也將步入幾個大的方向&#xff0c;Java網絡編程就是其中之一。 如今強調網絡的程序不比涉及網絡的更多。除了經典的應用程序&#xff0c;如電子郵件、Web瀏覽器和遠程登陸外&…

Navigator 對象,能夠清楚地知道瀏覽器的相關信息

Navigator 對象屬性 appCodeName屬性 功能&#xff1a;返回瀏覽器的代碼名。該屬性是一個只讀的字符串。 語法&#xff1a;navigator.appCodeName 總結&#xff1a;在所有以Netscape代碼為基礎的瀏覽器中&#xff0c;它的值是"Mozilla"。為了兼容起見&#xff0c;在M…

Jerry和您聊聊Chrome開發者工具

2019獨角獸企業重金招聘Python工程師標準>>> Chrome開發者工具是Jerry日常工作使用的三大調試器之一。雖然工具名稱前面帶了個"開發者", 但是它對非開發人員仍然有用。不信&#xff1f; 用Chrome打開我們常用的網站&#xff0c;按F12&#xff0c;在Consol…

BZOJ4314 倍數?倍數!

好神仙啊.... 題意 在$ [0,n) $中選$ k$個不同的數使和為$ n$的倍數 求方案數 $ n \leq 10^9, \ k \leq 10^3$ 題解 k可以放大到1e6的 先不考慮$ k$的限制 對答案構建多項式$ f(x)\prod\limits_{i0}^{n-1}(x^i1)$ 答案就是這個多項式所有次數為$ n$的倍數的項的系數和 考慮單位…

win2008R2管理員密碼修改文檔

場景&#xff1a;忘記了win2008R2服務器的管理員密碼。解決辦法&#xff1a;1、 制作一個U盤啟動盤&#xff1a;2、 系統通過U盤啟動進入WINpe系統3、 在知道Win2008安裝位置的情況下&#xff1b;查找C:\windows\system32\osk.exe 將osk.exe文件修改為&#xff1a;osk.exe.bat&…

Python檔案袋( 面向對象 )

類即是一個模型&#xff0c;根據模型建立起不同的對象&#xff0c;對象間擁有共同的一些屬性 簡單的類&#xff1a; 1 class P:2 #類變量&#xff0c;所有實例共享變量,推薦使用方法是&#xff1a;類名.類變量名3 pvarx"ppvar1"4 5 #構造函數6 def _…

javascript中的后退和刷新

轉自&#xff1a;https://www.cnblogs.com/tylerdonet/p/3911303.html <input typebutton value刷新 οnclick"window.location.reload()"><input typebutton value前進 οnclick"window.history.go(1)"><input typebutton value后退 οncl…

第四周

7-2 選擇法排序 &#xff08;20 分) 本題要求將給定的n個整數從大到小排序后輸出。 輸入格式&#xff1a; 輸入第一行給出一個不超過10的正整數n。第二行給出n個整數&#xff0c;其間以空格分隔。 輸出格式&#xff1a; 在一行中輸出從大到小有序的數列&#xff0c;相鄰數字間有…

checkPathValidity 檢查所有agent的corridor的m_path是否有效

在checkPathValidity&#xff08;檢查所有agent的corridor的m_path是否有效&#xff09; 如果是無效的要進行重新設置并且設置replan 首先獲得第一個polygon&#xff0c;m_path[0] 這里&#xff0c;因為addagent的時候&#xff0c;ag->corridor.reset(ref, nearest); m_path…

來談談JAVA面向對象 - 魯班即將五殺,大喬送他回家??

開發IDE為Eclipse或者MyEclipse。 首先&#xff0c;如果我們使用面向過程的思維來解決這個問題&#xff0c;就是第一步做什么&#xff0c;第二步做什么&#xff1f; 魯班即將五殺&#xff0c;大喬送他回家 這個現象可以簡單地拆分為兩步&#xff0c;代碼大概是這個樣子的: publ…

Vue 教程第一篇——基礎概念

認識 Vue 關于 Vue 的描述有不少&#xff0c;不外乎都會拿來與 Angular 和 React 對比&#xff0c;同樣頭頂 MVVM 雙向數據驅動設計模式光環的 Angular 自然被對比的最多&#xff0c;但到目前為止&#xff0c;Angular 在熱度上已明顯不及 Vue&#xff0c;性能已成為最大的詬病。…

Microsoft Teams的Outgoing Webhook開發入門

Microsoft Teams的應用程序有幾種形式&#xff1a; TabsBotsConnectorsMessaging extensionsActivity feed integrationsOutgoing web hooks 這篇我們主要介紹如何使用 ASP.NET Core來開發最簡單的Outgoing web hook。 什么是outgoing webhook Outgoing webhooks allow you t…

0418 jQuery筆記(添加事件、each、prop、$(this))

1.添加點擊事件、each、prop、$(this) 1 //全選框的被動操作2 //定義一個標志保存最終狀態3 var flag false;4 //為每一個選擇框添加點擊事件&#xff0c;數組.click()5 $(.chex).click(function(){6 //遍歷數組&#xff0c;數組.each()7 …