Android Studio实现购物商城界面:从源码到性能优化全攻略
2026/9/23 22:01:29 网站建设 项目流程

简介:面向Android初学者的购物商城界面源码工程,基于Android Studio与ListView组件开发,完整演示了商品列表的构建链路:从activity_main.xml中添加ListView、编写item_product自定义条目,到创建ProductAdapter并重写getCount、getItem、getItemId、getView四个核心方法,覆盖Android UI设计、数据绑定与适配器模式等关键知识点。压缩包共518个文件,整体约9.05MB,包含xml布局与配置、java/class源码、gradle构建脚本、png/jpg商品图片素材以及可直接安装的apk包,目录结构清晰,适合按模块阅读和运行调试,能够完整支撑从界面到逻辑的对照学习。目前已有13540人学习下载,在Android入门类资源中热度较高。透过源码可掌握ListView列表数据绑定、多条目视图复用、滚动监听与模拟数据加载等实用技巧,也可继续扩展下拉刷新、异步加载图片等功能,既适合初学者系统练习,也能作为移动应用课程设计的参考实现。

1. 一个购物商城界面,为什么值得从源码做起

很多人会觉得“Android Studio实现简单的购物商城界面”是个入门练手项目,不值得花心思。但真做过的人知道,把一个商城主页做到“能看、能点、不闪退、不卡顿”,背后是 XML 布局、资源管理、Fragment 生命周期、RecyclerView 复用机制和图片加载这几座大山的综合体。这个标题的价值就在于:它逼着你把 Android 界面开发里最高频、最容易翻车的部分,用一个完整的场景串起来。本文我会用 Android Studio 从零搭一个可运行的购物商城界面,把底部导航、首页轮播、双列商品流、数据模拟这几块的源码思路和参数取舍讲透,让你拿到就能改、改完就能跑。

2. 工程骨架先行:Gradle、清单与主题,先把界面运行的底座搭对

2.1 用 Empty Activity 起步,还是用第三方模板?

我见过不少同学为了省事,直接去下载网上那种“商城完整源码”,结果打开项目光同步依赖就花半天,跑起来还报一堆版本冲突。其实做购物商城界面,起步越干净越好。Android Studio 新建项目时选Empty Views Activity,比Empty Activity更合适,因为后者默认带的是 Compose 模板,而我们这里要控制的是传统 XML 界面。选 Java 还是 Kotlin 都可以,但如果是新学,我建议直接 Kotlin,后面接业务逻辑的时候代码量能少三分之一。

项目名建议叫MallDemo,包名用com.example.mall。这里有个小细节:包名里的example后面要改成自己的域名反写,不然以后要上架或接第三方 SDK 时,改包名是件非常痛苦的事,血泪经验。

创建完工程后,第一时间确认三个文件存在且正常:app/build.gradleAndroidManifest.xmlres/layout/activity_main.xml。这三样是界面的地基,后面所有代码都围绕它们展开。

2.2 三个必须提前改的 Gradle 配置

打开app/build.gradle,默认配置能用,但要做界面开发,有三个地方必须改。第一个是viewBinding,开启它之后就不用写一堆findViewById,界面代码会干净很多。第二个是依赖版本,把compileSdktargetSdk保持一致,避免出现“编译能用、安装到新手机闪退”的情况。第三个是添加图片加载库,商城界面离不开网络图或本地图,Glide 是目前最稳的选择。

android { namespace 'com.example.mall' compileSdk 34 defaultConfig { applicationId "com.example.mall" minSdk 24 targetSdk 34 versionCode 1 versionName "1.0" } buildFeatures { viewBinding true } } dependencies { implementation 'androidx.core:core-ktx:1.12.0' implementation 'androidx.appcompat:appcompat:1.6.1' implementation 'com.google.android.material:material:1.11.0' implementation 'androidx.constraintlayout:constraintlayout:2.1.4' implementation 'androidx.recyclerview:recyclerview:1.3.2' implementation 'androidx.viewpager2:viewpager2:1.0.0' implementation 'com.github.bumptech.glide:glide:4.16.0' }

这段配置里,viewBinding true是界面开发的关键开关,开启后会自动生成与 XML 文件名对应的绑定类,比如activity_main.xml会生成ActivityMainBindingminSdk 24意味着最低支持 Android 7.0,商城类项目一般不需要再往下兼容了,能省掉很多系统适配的麻烦。Glide 的4.16.0是目前比较稳定的版本,不要随手写个最新版,不然可能遇到依赖冲突。

改完 Gradle 后记得点一下Sync Now。如果同步失败,先看错误提示里有没有requires xxx version之类的描述,基本能定位到某个库版本不兼容,手动降到提示的版本就行。

2.3 主题与状态栏:商城的“第一屏观感”从这里定

商城主页能不能留住用户,第一眼是状态栏和标题栏的颜色。默认新建项目的主题是白底黑字,谈不上难看,但离商城的质感差很远。我一般会定义一个浅色主题,把状态栏和导航栏的颜色统一成品牌色。

新建res/values/themes.xml,内容改成这样:

<resources xmlns:tools="http://schemas.android.com/tools"> <style name="Theme.MallDemo" parent="Theme.MaterialComponents.DayNight.NoActionBar"> <item name="colorPrimary">#FF4D4F</item> <item name="colorPrimaryVariant">#D9363E</item> <item name="colorOnPrimary">#FFFFFF</item> <item name="android:statusBarColor">#FFFFFF</item> <item name="android:windowLightStatusBar">true</item> <item name="android:navigationBarColor">#FFFFFF</item> </style> </resources>

parentTheme.MaterialComponents.DayNight.NoActionBar是为了去掉自带 ActionBar,商城的标题栏一般是用自定义布局做,这样更灵活。windowLightStatusBartrue时状态栏图标会变深色,配合白色状态栏背景可读性更高。这里用的是接近电商“淘宝红”的色值,你也可以换成自己品牌的色号,改colorPrimary一处就行。

还有一个小技巧:商城首页的搜索框很多是顶在状态栏下面的,如果你的布局想实现“沉浸式效果”,可以在MainActivity里加两行代码:

WindowCompat.setDecorFitsSystemWindows(window, false)

开启后布局会延伸到状态栏后面,需要自己在布局里加android:fitsSystemWindows="true"来道让内容避开系统栏。这个属于界面细节,新手可以先不开,等布局稳定了再折腾。

3. 底部导航与 Fragment 容器:页面切换的两种写法与我的取舍

3.1 底部导航栏的基本实现

购物商城界面通常有四个页面:首页、分类、购物车、我的。底部导航用 Material 组件库里的BottomNavigationView实现,比自定义要稳得多。先创建res/menu/bottom_nav_menu.xml

<?xml version="1.0" encoding="utf-8"?> <menu xmlns:android="http://schemas.android.com/apk/res/android"> <item android:id="@+id/nav_home" android:icon="@drawable/ic_home" android:title="首页" /> <item android:id="@+id/nav_category" android:icon="@drawable/ic_category" android:title="分类" /> <item android:id="@+id/nav_cart" android:icon="@drawable/ic_cart" android:title="购物车" /> <item android:id="@+id/nav_profile" android:icon="@drawable/ic_profile" android:title="我的" /> </menu>

图标资源需要放在res/drawable目录下,可以用 Vector Asset 生成,右键res/drawableNewVector Asset,选系统自带的图标就行,不需要自己画。这里有个容易踩的坑:矢量图标的fillColor默认是黑色,放进BottomNavigationView后会跟着itemIconTint走,如果你不改itemIconTint,选中的状态就区分不出来。

接着在布局文件里把底部导航和 Fragment 容器搭起来。注意这里不推荐用两个LinearLayout把各自包起来,而是用ConstraintLayout做根布局,约束关系更清晰:

<?xml version="1.0" encoding="utf-8"?> <androidx.constraintlayout.widget.ConstraintLayout 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"> <FrameLayout android:id="@+id/fragmentContainer" android:layout_width="0dp" android:layout_height="0dp" app:layout_constraintTop_toTopOf="parent" app:layout_constraintBottom_toTopOf="@id/bottomNav" app:layout_constraintStart_toStartOf="parent" app:layout_constraintEnd_toEndOf="parent" /> <com.google.android.material.bottomnavigation.BottomNavigationView android:id="@+id/bottomNav" android:layout_width="0dp" android:layout_height="wrap_content" app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintStart_toStartOf="parent" app:layout_constraintEnd_toEndOf="parent" app:menu="@menu/bottom_nav_menu" app:itemIconTint="@color/nav_item_color" app:itemTextColor="@color/nav_item_color" /> </androidx.constraintlayout.widget.ConstraintLayout>

FrameLayout作为 Fragment 的容器是通用做法,因为它没有复杂的测量规则,切换 Fragment 时开销小。itemIconTintitemTextColor指向同一个颜色选择器,这个选择器要在res/color/nav_item_color.xml里定义,选中和未选中两套颜色,否则底部导航的颜色变化会不可控。

3.2 用 show/hide 方式管理 Fragment:防重建的实用写法

页面切换有两种主流方案:一种是FragmentTransaction.replace,每次切换都销毁旧 Fragment、创建新 Fragment;另一种是show/hide,所有 Fragment 只创建一次,之后只做显示和隐藏。对于商城这类需要保持页面状态的场景,我倾向于用show/hide。原因很直接:用户从首页滑到一半,切去购物车再切回来,如果页面重建了,滚动位置就丢了,还得重新加载图片,体验很差。

具体实现上,可以写一个简单的封装,在MainActivity里管理四个 Fragment:

class MainActivity : AppCompatActivity() { private lateinit var binding: ActivityMainBinding private val homeFragment = HomeFragment() private val categoryFragment = CategoryFragment() private val cartFragment = CartFragment() private val profileFragment = ProfileFragment() private var currentFragment: Fragment = homeFragment override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) binding = ActivityMainBinding.inflate(layoutInflater) setContentView(binding.root) supportFragmentManager.beginTransaction() .add(R.id.fragmentContainer, homeFragment, "home") .add(R.id.fragmentContainer, categoryFragment, "category") .add(R.id.fragmentContainer, cartFragment, "cart") .add(R.id.fragmentContainer, profileFragment, "profile") .hide(categoryFragment) .hide(cartFragment) .hide(profileFragment) .commit() binding.bottomNav.setOnItemSelectedListener { item -> when (item.itemId) { R.id.nav_home -> switchFragment(homeFragment) R.id.nav_category -> switchFragment(categoryFragment) R.id.nav_cart -> switchFragment(cartFragment) R.id.nav_profile -> switchFragment(profileFragment) } true } } private fun switchFragment(target: Fragment) { if (target == currentFragment) return supportFragmentManager.beginTransaction() .hide(currentFragment) .show(target) .commit() currentFragment = target } }

这里有个细节:四个 Fragment 在onCreate里一次性全部add进去,然后隐藏三个。好处是后面切换时完全没有创建耗时,但代价是应用启动时会同时执行四个 Fragment 的初始化逻辑,如果里面都有网络请求,就会造成流量浪费。所以在写 Fragment 时,懒加载是一个重要习惯:只在onResume里根据“是否第一次显示”来决定要不要拉数据。

3.3 分类页和购物车页先做静态占位,别急着填功能

很多人在做商城界面时容易犯一个错:一上来就想着把购物车的增删改查全做了,结果界面没做完,心态先崩。建议前两版里,分类页、购物车页、我的页先用静态占位布局,只保证页面切换不崩、样式统一,把精力集中在首页商品流上。占位页用一个TextView居中显示页面名称就好,等首页稳定了再逐个填充。

购物车可以做个显示角标的小功能,不复杂,但对“像商城”有立竿见影的效果。底部导航菜单在 XML 里给购物车 item 加app:badgeText="3",就能显示角标。如果要动态更新,可以这样写:

val badge = binding.bottomNav.getOrCreateBadge(R.id.nav_cart) badge.isVisible = true badge.number = cartCount

getOrCreateBadge是 Material 库提供的接口,number超过 99 时会自动显示 99+,这个细节很符合真实商城的行为。别小看这几行代码,它让整个界面从“静态图”变成了“有反馈的页面”,用户的观感完全不同。

4. 首页内容区:轮播图与双列商品流的落地代码

4.1 商品数据怎么来,先别接后端

购物商城的界面开发,最怕的就是“等接口”。后端还没好,界面就只能干瞪眼。正确的做法是先用本地模拟数据把界面跑通,再换接口。我一般在项目里建一个MockData.kt,把商品图、名称、价格写死成列表,等接口通了只替换数据源就行。

data class Product( val name: String, val price: String, val imageUrl: String ) object MockData { val products = listOf( Product("无线蓝牙耳机 Pro", "299", "https://example.com/headphone.png"), Product("智能运动手环", "199", "https://example.com/band.png"), Product("便携榨汁杯", "89", "https://example.com/juicer.png") ) }

这里的imageUrl我用了网络图片地址,实际开发中图片资源建议先放本地drawable,用R.drawable.xxx占位。原因很简单:网络图片要等加载,加载失败显灰图,会干扰你对布局的判断。先用本地图把位置、尺寸调准,再换网络图,效率高得多。MockData写成object单例,是 Kotlin 里管理静态数据的常用方式,全局只有一份实例,不会产生多余的内存开销。

4.2 RecyclerView 双列商品流:从布局到适配器

商品列表是整个商城界面的核心,用RecyclerViewGridLayoutManager实现双列网格是通用做法。先写列表条目布局item_product.xml,这里要注意图片和文字的比例关系:

<?xml version="1.0" encoding="utf-8"?> <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" android:layout_width="match_parent" android:layout_height="wrap_content" android:orientation="vertical" android:background="#FFFFFF" android:padding="8dp"> <ImageView android:id="@+id/productImage" android:layout_width="match_parent" android:layout_height="0dp" android:scaleType="centerCrop" /> <TextView android:id="@+id/productName" android:layout_width="match_parent" android:layout_height="wrap_content" android:maxLines="2" android:ellipsize="end" android:textSize="14sp" android:layout_marginTop="6dp" /> <TextView android:id="@+id/productPrice" android:layout_width="wrap_content" android:layout_height="wrap_content" android:textColor="#FF4D4F" android:textSize="16sp" android:textStyle="bold" android:layout_marginTop="4dp" /> </LinearLayout>

这里图片高度用0dp是个关键细节。在双列网格里,如果图片高度固定,不同商品的图比例不同,会产生大量空白。0dp配合adjustViewBounds可以让图片按比例撑开,但要注意ImageView的宽高比会跟着图片原始比例走,导致每行高度不一致。如果想让每张卡片高度统一,更稳妥的做法是给图片一个固定的aspectRatio,比如用ConstraintLayoutapp:layout_constraintDimensionRatio="1:1",强制图片区域正方形。两种方式各有适用场景,商品图一般建议正方形,视觉上更整齐。

适配器写法如下:

class ProductAdapter( private val products: List<Product>, private val onClick: (Product) -> Unit ) : RecyclerView.Adapter<ProductAdapter.ProductViewHolder>() { class ProductViewHolder(val binding: ItemProductBinding) : RecyclerView.ViewHolder(binding.root) override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ProductViewHolder { val binding = ItemProductBinding.inflate( LayoutInflater.from(parent.context), parent, false ) return ProductViewHolder(binding) } override fun onBindViewHolder(holder: ProductViewHolder, position: Int) { val product = products[position] holder.binding.productName.text = product.name holder.binding.productPrice.text = "¥${product.price}" holder.binding.root.setOnClickListener { onClick(product) } } override fun getItemCount(): Int = products.size }

onCreateViewHolder只负责创建视图,onBindViewHolder负责绑数据,这是 RecyclerView 复用的核心机制。每次滚动时onBindViewHolder都会被调用,所以耗时的操作绝不能放在这里,比如从磁盘读大图或者做复杂的字符串拼接。上面代码里价格拼接用字符串模板就可以了,不要在这里做任何 IO 操作。

在 Fragment 里配置列表:

binding.recyclerView.layoutManager = GridLayoutManager(requireContext(), 2) binding.recyclerView.adapter = ProductAdapter(MockData.products) { product -> Toast.makeText(requireContext(), "点击了 ${product.name}", Toast.LENGTH_SHORT).show() }

GridLayoutManager第一个参数传 2 就是双列。如果想让两列之间有点间距,需要自己写ItemDecoration,RecyclerView 本身不支持直接设置间距,这是新手最容易迷惑的一点。一个简单的间距实现是继承RecyclerView.ItemDecoration,在getItemOffsets里根据 position 计算偏移。固定间距的做法是每个 item 四周各4dp,这样肉眼看到的就是8dp的列间距,视觉均匀。

4.3 轮播图塞进列表头部:用装饰器实现混合布局

购物商城的首页,轮播图通常在商品列表的上方,而且会跟着列表一起滚动。新手常见的错误是把轮播图放在列表外面,再用一个垂直ScrollView包住整个页面。这样做会带来两个致命问题:一是ListViewRecyclerViewScrollView会产生滑动冲突,二是所有 item 一次性加载,列表长一点就会内存暴涨。

正确做法是把轮播图作为商品列表的第一个 item,用RecyclerViewaddHeaderView思路实现。但RecyclerView原生不支持头部,常见方案有两种:一种是用ConcatAdapter把两个适配器拼起来,另一种是修改适配器的 item 类型。ConcatAdapter的代码更直观:

val bannerAdapter = BannerAdapter(bannerImages) val productAdapter = ProductAdapter(MockData.products) { } val concatAdapter = ConcatAdapter(bannerAdapter, productAdapter) binding.recyclerView.adapter = concatAdapter

BannerAdapter内部用ViewPager2实现真正的轮播,注意它要占满横向宽度,高度建议设置成宽度的一半左右,比如屏宽 400dp 时高度 200dp,这样轮播图的比例接近 2:1,和主流电商风格一致。ViewPager2的自动轮播用Handler.postDelayed实现,每 3 秒切换一页,切到最后一页后回到第一页,循环模式直接设置android:repeatMode="restart"即可,不用自己写取模逻辑。

自动轮播的代码放 Fragment 生命周期里要注意释放,不然 Fragment 销毁后Handler还在跑,轻则内存泄漏,重则空指针崩溃。规范写法如下:

private val handler = Handler(Looper.getMainLooper()) private val bannerRunnable = object : Runnable { override fun run() { val next = if (bannerAdapter.currentItem < bannerAdapter.itemCount - 1) { bannerAdapter.currentItem + 1 } else { 0 } bannerAdapter.setCurrentItem(next, true) handler.postDelayed(this, 3000) } } override fun onResume() { super.onResume() handler.postDelayed(bannerRunnable, 3000) } override fun onPause() { super.onPause() handler.removeCallbacksAndMessages(null) }

removeCallbacksAndMessages(null)会清空该 Handler 所有待执行任务,在onPause里调用是安全的,不会影响 UI 线程其他消息。轮播的指示器小圆点可以用TabLayoutMediator实现,这个类就是为了把指示器和 ViewPager2 绑定而设计的,代码量少且稳定。

5. 界面开发五大坑:现象、原因、解决方案

5.1 轮播图和商品列表互相抢滑动,页面像卡住了一样

现象:手指放在轮播图上上下滑动时,列表不动或者只有轮播图自己在动,体验非常生硬。

原因:ViewPager2本身处理的是水平滑动,垂直方向的触摸事件默认不会传给父级RecyclerView。两者在事件分发上形成了竞争,如果不干预,列表就感知不到垂直滑动,给人“卡界面”的感觉。

解决:不需要写复杂的事件拦截代码,ViewPager2官方就支持嵌套滚动。检查RecyclerView是否设置了android:nestedScrollingEnabled="true",并且ViewPager2所在 item 外部不要包裹会消费触摸事件的布局。如果还冲突,就在ViewPager2的根布局上加一行:

android:overScrollMode="never"

把轮播图越界时的发光效果去掉,交互会顺滑很多。这是我在真机上反复对比过的,模拟器上表现不明显,但低端安卓机上差别很大。

5.2 Glide 加载图片失败,占位图一直转圈

现象:商品图加载不出来,本地图能显示,换成网络图就是一直转圈,重试也没用。

原因:最常见的是网络权限没加,AndroidManifest.xml里没有声明INTERNET权限。另一个高发原因是图片 URL 是http://明文协议,从 Android 9(API 28)开始默认禁止明文流量,Glide 会直接报错,日志里能看到CLEARTEXT communication not permitted

解决:先在 manifest 确认权限是否齐全:

<uses-permission android:name="android.permission.INTERNET" />

然后处理明文流量,在AndroidManifest.xml<application>标签上加一行:

android:usesCleartextTraffic="true"

这种做法只适合开发环境,上线前还是要换成https://的图片地址,否则应用市场审核会有风险。临时调试用明文,正式环境切 https,这是通用规则。

5.3 RecyclerView 条目复用导致内容错乱:图片和文字对不上

现象:列表快速滚动时,某几个 item 显示的商品名称和图片不匹配,停下来又恢复正常。

原因:这是 RecyclerView 复用的经典问题。onBindViewHolder只更新了文本和图片,但如果图片加载是异步的,item 被复用到另一个商品上时,前一个商品的图片才加载完成,就直接显示在当前 item 上了,因为ImageView没有清除旧状态。

解决:在绑定图片时,先清掉旧图,再设置新图。Glide 里用override固定尺寸可以加速加载,同时保证所有商品图显示尺寸统一:

Glide.with(holder.itemView.context) .load(product.imageUrl) .placeholder(R.drawable.ic_placeholder) .error(R.drawable.ic_error) .centerCrop() .into(holder.binding.productImage)

centerCrop会裁剪图片填满控件,规避不同图片比例带来的高度抖动。占位图和错误图一定都要配,否则加载失败时显示的是空白区域,用户会以为是 bug。

5.4 底部导航切换回来,页面状态全丢了

现象:首页商品列表滚到第十几个,切到购物车再切回来,列表回到了顶部,轮播图也重新开始播。

原因:如果用的是replace方式切换 Fragment,每次切换都会销毁页面视图,RecyclerView的滚动位置自然保不住。另一个原因是在onCreateView里每次都重新 inflate 布局,虽然没有 replace,但视图重建了,状态一样丢。

解决:用前面第 3.2 节说的show/hide方式管理 Fragment,R布局只 inflate 一次。如果项目里 Fragment 数量多,可以考虑用FragmentStateAdapter+ViewPager2配合offscreenPageLimit做页面的预加载和状态保持:

viewPager2.offscreenPageLimit = 2

offscreenPageLimit表示当前页左右各缓存多少页,设置成 2 意味着四个页面基本都活着,切页时不会触发页面重建。这个参数不是越大越好,每多一页就多一份内存占用,商城场景 2 是一个平衡值。

5.5 深色模式下界面颜色全乱了

现象:把手机切到深色模式,商城主页白底变黑底,但文字和图标颜色没跟着变,出现浅色文字配深色图片的诡异效果。

原因:主题继承的是DayNight系列,系统切换深色模式后会自动套用夜间主题,但布局文件里硬编码了#FFFFFF背景和#333333文字,这些颜色不会跟随主题变化。

解决:在res/values-night/themes.xml里单独定义深色主题下的配色,布局中引用颜色资源而不是直接写十六进制。另一个更实用的方案是商城类 app 一般只做浅色模式,把主题父类改成不带DayNight的版本,并在应用主题里强制关闭深色模式:

<style name="Theme.MallDemo" parent="Theme.MaterialComponents.Light.NoActionBar"> <item name="android:forceDarkAllowed">false</item> </style>

forceDarkAllowed设为 false 后,系统不会对应用启用力适配深色,界面始终保持在浅色设计稿的观感下。这个方案虽然“不先进”,但真实商城 app 多数就是这么做的,因为深色模式下商品的展示效果很难保证,改起来成本高。

6. 做完界面之后:布局验证与性能收尾技巧

界面跑通不算完,真正的问题是“在用户手里跑得顺不顺”。我之前吃过一次亏:模拟器上一切正常,到了一台 4GB 内存的旧手机上,列表滑动肉眼可见的掉帧,查了半天发现是商品 item 里嵌套了一层多余的LinearLayout,导致测量时间翻倍。所以界面做完了,一定要用工具验证布局是否有过度绘制、层级是否过深。

Android Studio 自带的Layout Inspector是排查布局层级的第一利器。运行项目后,点菜单栏Tools → Layout Inspector,它会以 3D 视图展示当前界面的所有 View 层级。你要看的核心指标是“树的最大深度”,Android 官方建议不超过 10 层,超过这个值,低端机上的测量和绘制时间会显著上升。常见问题包括:ConstraintLayout里嵌套LinearLayoutFrameLayout里再套FrameLayout,这种嵌套大多可以合并。

布局验证完,再用GPU 渲染模式分析做性能体检。在真机开发者选项里开启Profile GPU rendering,选On screen as bars,然后操作页面 5 分钟。每个柱子的高度代表一帧的渲染耗时,如果柱子大面积超过屏幕顶部的绿线,说明有帧超时。这时候优先检查是不是有wrap_contentImageView在滑动时频繁触发重排,这类问题比代码逻辑更常见。

最后的收尾建议是固定 item 高度。商品列表的item_product.xml里,图片区固定为正方形比例,文本固定为两行maxLines,不要让 item 高度随内容浮动。高度不稳定是列表卡顿的最大原因,固定后 RecyclerView 不需要每次滑动都重新测量 item,整条列表的渲染开销会明显降下来。这也是为什么我在写 item 布局时强调图片用ConstraintLayoutdimensionRatio而不是让图片自适应比例,前者牺牲了一点点布局灵活性,换来的却是稳定的滚动性能。

界面开发没有太多的玄学,大部分问题都能在布局层和数据层找到根因。这个商城界面项目,价值不在于它多美观多复杂,而在于它把 Android 开发的高频场景全部覆盖了一遍:你把这些坑踩平,再做复杂项目,会发现基础的布局、复用、主题、适配思维能直接复用。希望这些踩坑记录能帮你在做界面时少交点充电费,少熬几个改 bug 的夜。

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

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

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

立即咨询