☰
安卓UI框架源码解析:底部导航、Fragment容器与主题切换实战
2026/10/24 2:49:04 网站建设 项目流程

简介:这份资源面向Android初中级开发者与需要搭建项目骨架的工程师,围绕底部导航栏定制、Fragment灵活切换与整体架构设计三个层面,提供一套可直接参考的UI框架实现。压缩包共214个文件,约5.25MB,以128个png界面切图、24个xml布局与配置、18个java源码及30个class文件为主,另含apk安装包、jar依赖与dex文件,覆盖从界面素材到可运行工程的完整链路。资源中可见TabView、FragmentUtils、ServiceFragment等类,配合TitleActivity、AboutActivity等页面,展示了底部任务栏与多Fragment切换的落地写法,便于读者对照理解FragmentManager事务管理与界面过渡处理。已有502人学习下载,适合希望减少重复搭建成本、快速理清UI分层与代码组织方式的开发者参考借鉴。

1. 拿到一个安卓UI框架源码,先别急着改布局

很多安卓项目做到第二个月就开始还债:底部导航用着用着要加小红点,某个页面要隐藏底栏,换肤需求一来发现颜色全写死在XML里。我最近拆的一个「android项目整体UI框架」源码包,就是冲着这些痛点去的——它把底部导航、Fragment容器、主题切换、状态栏适配这几件事收进一套可复用的壳里,业务页面只管往里塞。适合两类人:一是刚接手公司老项目、想给UI层做一次结构梳理的开发者;二是想找一个能直接跑起来的安卓框架源码当脚手架、省掉搭架子时间的人。下面按「它长什么样 → 怎么跑起来 → 核心怎么实现 → 怎么改」的顺序拆,中间会穿插我踩过的坑。

2. 框架整体结构与技术选型:从目录看它想解决什么

2.1 目录分层与模块职责

拿到源码包,先别打开Android Studio,用文件管理器把目录扫一遍,能省掉后面一半的迷茫。这个框架的目录结构是典型的「壳 + 业务」两层,常见做法是这样组织的:

app/ # 壳工程,只放启动页和全局配置 src/main/java/.../ MainActivity.java # 唯一Activity,承载所有Fragment base/ BaseActivity.java # 公共Activity基类 BaseFragment.java # 公共Fragment基类 BaseApplication.java # 全局初始化 ui/ home/ # 首页业务 discover/ # 发现页业务 mine/ # 我的页业务 widget/ BottomTabBar.java # 自定义底部导航栏 TabItem.java # 单个Tab的数据模型 theme/ ThemeManager.java # 主题/换肤管理 StatusBarUtil.java # 状态栏适配工具 src/main/res/ layout/activity_main.xml # 壳布局:容器 + 底栏 values/colors.xml # 颜色统一入口 values/styles.xml # 主题样式

这个分层的核心思路是「单Activity + 多Fragment」。为什么不用多Activity?因为底部导航切换时如果走Activity跳转,转场动画、状态保存、返回栈都会变复杂,而Fragment切换只动容器里的内容,底栏纹丝不动,体验更接近主流App。base/目录放的是所有页面共享的生命周期钩子和公共逻辑,widget/放自绘控件,theme/放全局外观控制,业务代码全部隔离在ui/下。这种划分的好处是:换肤、改底栏样式、加全局 loading,都只动base和widget,不碰业务。

2.2 技术选型:为什么是原生 View 而不是 Compose

源码里没有用 Jetpack Compose,全部是原生 View 体系加 support/androidx 兼容库。这不是落后,而是这个框架的定位决定的——它要能塞进存量项目。很多公司的老项目还在用 XML 布局和 findViewById,如果框架上来就是 Compose,迁移成本直接劝退。原生 View 的好处是:布局文件直观、和现有代码零摩擦、自定义控件(比如带红点和动画的底栏)用 Canvas 或 ViewGroup 写起来可控。

依赖层面,除了基础的 appcompat、fragment、constraintlayout,框架还引了lifecycle做生命周期感知,viewmodel做页面数据持有。这几个库的组合是当前原生安卓项目的稳妥配置,不追新但够用。有一点要注意:源码里如果用的是android.support.*而不是androidx.*,说明它是老版本,导入前得先做 AndroidX 迁移,否则新版本 Android Studio 直接编译报错。这个判断方法很简单,打开build.gradle看依赖前缀就行。

2.3 壳布局的骨架:容器 + 底栏的两段式结构

整个框架的视觉骨架就在activity_main.xml里,结构非常克制:

<LinearLayout android:layout_width="match_parent" android:layout_height="match_parent" android:orientation="vertical"> <!-- 内容容器:所有Fragment都塞这里 --> <FrameLayout android:id="@+id/container" android:layout_width="match_parent" android:layout_height="0dp" android:layout_weight="1" /> <!-- 底部导航:自绘控件,高度固定 --> <com.example.widget.BottomTabBar android:id="@+id/bottom_tab_bar" android:layout_width="match_parent" android:layout_height="56dp" /> </LinearLayout>

FrameLayout用layout_weight="1"占满剩余空间,底栏固定 56dp,这是 Material Design 里底部导航的标准高度。容器用 FrameLayout 而不是 FragmentContainerView,是因为源码可能兼容较老的 fragment 版本,FrameLayout 更通用。这里有个细节:容器 id 必须和MainActivity里FragmentManager替换时用的 id 一致,改布局时如果顺手改了 id 忘了改代码,运行起来就是白屏,这个坑我后面会细说。

3. 跑起来:从导入到第一个页面切换的完整步骤

3.1 环境准备与导入

先把环境对齐,不然打开就是一片红。源码包对环境的常见要求是:Android Studio 4.0 以上、Gradle 6.x、compileSdk 29 或 30、minSdk 21。导入步骤:

# 1. 解压后进入项目根目录,确认有 gradlew 文件 ls -la | grep gradlew # 2. 命令行先跑一次编译,比在IDE里点Build更快暴露问题 ./gradlew assembleDebug # 3. 如果报 AndroidX 相关错误,检查 gradle.properties grep "android.useAndroidX" gradle.properties

第三步是关键。如果gradle.properties里没有android.useAndroidX=true和android.enableJetifier=true这两行,而依赖里又混了 support 库,编译会直接失败。加上这两行后,Jetifier 会自动把老库的包名映射到 androidx,省掉手动改 import 的功夫。命令行编译还有个好处:错误信息完整,不像 IDE 的 Build 窗口经常把根因折叠掉。

3.2 首次运行与底栏切换验证

编译通过后连真机或模拟器跑起来,第一件事不是看界面好不好看,而是验证底栏切换是否正常。点三个 Tab,观察三件事:内容区是否跟着换、底栏选中态是否同步、快速连点会不会崩。框架里切换逻辑通常长这样:

// MainActivity 中的 Tab 切换处理 private void switchFragment(int index) { Fragment target = fragmentList.get(index); FragmentTransaction tx = getSupportFragmentManager().beginTransaction(); // 隐藏当前显示的,显示目标,避免重复 add 导致重叠 for (Fragment f : fragmentList) { if (f != target && f.isAdded()) { tx.hide(f); } } if (!target.isAdded()) { tx.add(R.id.container, target); } else { tx.show(target); } tx.commitAllowingStateLoss(); // 底栏切换场景用这个,避免状态丢失崩溃 }

这段代码的逻辑是「懒加载 + 显隐切换」:Fragment 第一次点才 add,之后只做 show/hide,不销毁重建,所以页面状态能保留。commitAllowingStateLoss而不是commit,是因为底栏点击可能发生在 Activity 状态保存之后(比如快速切后台再切回来),用commit会抛IllegalStateException。参数index对应底栏 Tab 的下标,fragmentList在onCreate里初始化,顺序必须和底栏 Tab 顺序严格一致,错一位就是内容对不上。

3.3 主题与状态栏适配的验证

跑通切换后,验证换肤和状态栏。框架的ThemeManager一般提供applyTheme(int colorRes)这类方法,调用后遍历当前页面刷新颜色。状态栏适配则看StatusBarUtil,核心是设置状态栏透明并调整内容 padding,避免内容被状态栏遮挡。验证方法:进设置页切一次主题,再进一个带图片头部的页面,看图片是否顶到状态栏下面、文字是否被挡。如果被挡,说明fitsSystemWindows或 padding 计算有问题,这是安卓 UI 框架里最高频的适配坑之一。

4. 核心模块拆解:底栏、Fragment 容器与主题管理怎么实现

4.1 自绘 BottomTabBar:一个 ViewGroup 撑起导航

底栏没有用BottomNavigationView,而是自绘BottomTabBar,原因是要支持红点、角标、自定义图标动画这些 BottomNavigationView 不好扩展的能力。核心实现思路是继承LinearLayout,按 Tab 数量等分添加子 View:

public class BottomTabBar extends LinearLayout { private List<TabItem> tabs = new ArrayList<>(); private int currentIndex = 0; private OnTabSelectedListener listener; public void setTabs(List<TabItem> items) { this.tabs = items; removeAllViews(); setWeightSum(items.size()); // 等分权重 for (int i = 0; i < items.size(); i++) { View tabView = buildTabView(items.get(i), i); addView(tabView, new LayoutParams(0, MATCH_PARENT, 1f)); } } private View buildTabView(TabItem item, final int index) { // 图标 + 文字 + 红点,用竖直 LinearLayout 组合 LinearLayout root = new LinearLayout(getContext()); root.setOrientation(VERTICAL); root.setGravity(Gravity.CENTER); root.setOnClickListener(v -> selectTab(index)); // ... 添加 ImageView 和 TextView,红点用 BadgeView 叠加 return root; } private void selectTab(int index) { if (index == currentIndex) return; // 重复点击不处理 currentIndex = index; refreshSelectedState(); // 遍历子View更新选中/未选中图标 if (listener != null) listener.onTabSelected(index); } }

关键点有三个:setWeightSum配合子 View 的weight=1实现等分,Tab 数量变化时不用改布局;selectTab里先判断index == currentIndex直接返回,避免重复点击触发无谓的 Fragment 切换;选中态刷新是遍历所有子 View 统一改,而不是只改新旧两个,这样红点、文字颜色、图标都能一起更新。TabItem是个简单数据类,持有选中/未选中图标资源、标题、红点数量。参数上,OnTabSelectedListener把切换事件抛给MainActivity,底栏本身不碰 Fragment,职责清晰。

4.2 Fragment 懒加载与状态保存

框架的BaseFragment里封装了懒加载逻辑,解决「ViewPager 或底栏预加载导致数据请求过早」的问题。常见实现是结合setUserVisibleHint(老方式)或onHiddenChanged(显隐切换方式):

public abstract class BaseFragment extends Fragment { private boolean isLoaded = false; // 是否已加载过数据 private boolean isVisible = false; // 当前是否可见 @Override public void onResume() { super.onResume(); isVisible = true; tryLoadData(); } @Override public void onPause() { super.onPause(); isVisible = false; } @Override public void onHiddenChanged(boolean hidden) { super.onHiddenChanged(hidden); isVisible = !hidden; tryLoadData(); } private void tryLoadData() { if (isVisible && !isLoaded) { isLoaded = true; onLazyLoad(); // 子类实现真正的数据加载 } } protected abstract void onLazyLoad(); }

逻辑说明:isLoaded保证数据只加载一次,isVisible跟踪可见性,两个条件同时满足才触发onLazyLoad。onHiddenChanged是 show/hide 切换时唯一会回调的方法,所以必须在这里更新可见性,否则从别的 Tab 切回来时数据不会刷新。参数上,子类只需实现onLazyLoad,不用关心生命周期判断。这里有个容易翻车的点:如果框架用的是replace而不是show/hide,onHiddenChanged根本不会回调,懒加载就失效了,得改用onResume判断。所以看源码时先确认切换方式,再决定懒加载挂哪个回调。

4.3 ThemeManager:颜色集中管理才能换肤

换肤能成立的前提是颜色不散落在各处。框架的做法是colors.xml里定义语义化颜色名(如color_primary、color_bg),ThemeManager持有当前主题的颜色映射,切换时通知注册过的 View 刷新:

public class ThemeManager { private static ThemeManager instance; private int primaryColor; private final List<OnThemeChangeListener> listeners = new ArrayList<>(); public static ThemeManager get() { if (instance == null) instance = new ThemeManager(); return instance; } public void applyTheme(int primaryRes) { this.primaryColor = ContextCompat.getColor(appContext, primaryRes); for (OnThemeChangeListener l : listeners) { l.onThemeChanged(primaryColor); } } public void register(OnThemeChangeListener l) { listeners.add(l); } public void unregister(OnThemeChangeListener l) { listeners.remove(l); } }

applyTheme接收颜色资源 id,解析成具体色值后广播给所有监听者。BaseActivity和BaseFragment在onCreate里注册、onDestroy里反注册,避免内存泄漏。业务页面实现onThemeChanged,在里面改背景色、文字色。这个模式的局限是只支持颜色级换肤,不支持整套 style 切换;如果要换字体、换圆角,得扩展成主题资源包方案。但对大多数项目来说,颜色换肤已经覆盖 80% 的需求,实现成本低,这是它务实的地方。

5. 避坑与常见问题:这几个坑我替你踩过了

5.1 白屏:容器 id 对不上

现象:App 启动后底栏正常显示,但内容区一片白,点 Tab 也没反应。原因:activity_main.xml里容器的 id 被改过(比如从container改成fragment_container),但MainActivity里tx.add(R.id.container, target)还是旧 id,add找不到目标容器直接失败,异常被吞掉。解决:全局搜R.id.后面跟的容器名,确保布局和代码一致;更稳妥的做法是把容器 id 抽成constants里的常量,两边引用同一个。改布局后先跑一次切换,别等打包才发现。

5.2 状态栏遮挡内容

现象:带图片头部的页面,图片顶部被状态栏盖住,或者标题文字顶到屏幕最上面。原因:Activity 设置了windowTranslucentStatus或fitsSystemWindows=false,但内容布局没有加对应的 padding。解决:在BaseActivity里统一处理,用StatusBarUtil.getStatusBarHeight()拿到状态栏高度,给根布局setPadding(0, height, 0, 0);或者给需要沉浸式的页面单独加android:fitsSystemWindows="true"。注意别在 Fragment 里重复加 padding,否则切换页面时 padding 会叠加,越切越往下。

5.3 快速连点底栏导致 Fragment 重叠

现象:手速快的时候连点两个 Tab,内容区出现两个 Fragment 叠在一起,或者返回键行为错乱。原因:commit()是异步的,连续两次事务在同一个FragmentManager上排队执行,前一个还没完成就开始了下一个,hide/show状态错位。解决:切换前判断fragmentManager.isStateSaved(),已保存就丢弃这次切换;或者用commitNow()同步执行(注意不能在onSaveInstanceState之后调用)。更简单的办法是在selectTab里加一个 300ms 的防抖,物理上杜绝连点。

5.4 换肤后部分控件颜色没变

现象:切主题后底栏和背景变了,但某些按钮、分割线还是旧颜色。原因:这些控件的颜色写死在布局 XML 里(android:textColor="#333333"),没走colors.xml的语义化引用,ThemeManager广播时它们没注册监听。解决:全局搜布局文件里的#开头的硬编码色值,全部替换成@color/xxx引用;然后在对应页面的onThemeChanged里刷新。血泪经验是:换肤功能能不能做干净,取决于前期颜色管理规不规范,后期补是补不完的。

5.5 返回栈与底栏状态不同步

现象:在「我的」页里进了二级页面,按返回键回到「我的」,但底栏选中态跳到了「首页」。原因:二级页面用了新的 Activity 或者手动replace了容器,返回时MainActivity的currentIndex没恢复。解决:二级页面统一用 Fragment 压在容器里,配合FragmentManager的返回栈;如果必须用新 Activity,在onResume里根据当前可见 Fragment 反查下标,重新设置底栏选中态。核心原则是:底栏选中态的唯一数据源应该是「当前显示的 Fragment」,而不是一个独立的currentIndex变量。

6. 二次开发:把框架改成自己项目的形状

拿到这套框架,真正的工作量在「改」而不是「跑」。我的习惯是先定三条改造线,再动手。

第一条线是底栏扩展。如果项目要加中间凸起的发布按钮,别去改BottomTabBar的等分逻辑,而是在setTabs之后单独addView一个悬浮按钮,用FrameLayout包一层,让凸起部分超出底栏高度。这样等分逻辑不受影响,红点、选中态也照常工作。改完验证方法:Tab 数量从 3 个加到 5 个,看等分是否还均匀、凸起按钮是否居中。

第二条线是页面容器升级。如果项目需要左右滑动切换,把FrameLayout换成ViewPager2,但要注意ViewPager2内部用的是RecyclerView,Fragment 的onHiddenChanged不会回调,懒加载得改挂onResume或者用setMaxLifecycle控制。这个改动影响面大,建议单独拉分支,改完把 5.3 的连点场景重测一遍。

第三条线是主题方案扩展。颜色换肤够用就留着,要支持深色模式就接AppCompatDelegate.setDefaultNightMode,把ThemeManager的颜色映射改成读values-night下的资源。这里有个技巧:先在colors.xml里把所有颜色改成?attr/colorPrimary这类主题属性引用,再切深色模式,大部分控件会自动适配,剩下的再手动补。

改造点改动文件验证重点
加凸起按钮BottomTabBar、activity_main.xml等分是否均匀、凸起是否居中
换 ViewPager2activity_main.xml、MainActivity懒加载是否生效、滑动是否卡顿
深色模式colors.xml、styles.xml、ThemeManager硬编码色值是否清理干净

最后说个教训。我早期改这类框架,习惯直接在主分支上动BaseFragment,结果一个懒加载改动把三个业务页面的数据刷新全带崩了,排查了半天才发现是onHiddenChanged和onResume的触发顺序问题。从那以后我每次动base目录的代码,都强制先写一个最小验证页——就一个 TextView 加一个日志,把生命周期回调全打出来,确认触发顺序符合预期再改业务。这个习惯帮我省掉了大量「改一处崩三处」的后悔药。希望帮到你。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询