一行代码实现底部导航栏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.mzph.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 …