手机App上的轮播图是如何实现的—探究安卓轮播图
安卓开发轮播图
安卓原生开发实现轮播图一般有两种方法,一种是直接使用ViewFlipper组件,另一种是基于ViewPager2实现轮播图。本文将对这两种方法进行讲解。ViewFlipper相较于ViewPager2用法比较简单,但是性能和效果相较ViewPager2就相对差一些,下面先来看ViewFlipper
ViewFlipper
ViewFlipper继承自VIewAnimator,而ViewAnimator继承自FrameLayout,是一个ViewGroup。它是安卓系统提供的原生ui组件,主要用于在多个子视图中实现带有动画效果的切换,常用于制作轮播图。作为一个ui组件,ViewFlipper的使用是比较简单的。
如果只需要轮播图片或者其他单个ui控件的话可以直接将imageview或控件放进ViewFlipper中,下面我们展示对布局文件进行轮播的例子。首先在布局中写一个ViewFlipper
<?xml version="1.0" encoding="utf-8"?><androidx.constraintlayout.widget.ConstraintLayoutxmlns:android="http://schemas.android.com/apk/res/android"xmlns:app="http://schemas.android.com/apk/res-auto"xmlns:tools="http://schemas.android.com/tools"android:id="@+id/main"android:layout_width="match_parent"android:layout_height="match_parent"tools:context=".MainActivity"><ViewFlipperandroid:id="@+id/viewflipper"android:layout_width="match_parent"android:layout_height="match_parent"/>
</androidx.constraintlayout.widget.ConstraintLayout>
写完ViewFlipper后再写对应的子项布局文件,下面以一个TextView和ImageView的简单组合为例
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"android:orientation="vertical"android:layout_width="match_parent"android:layout_height="match_parent"><TextViewandroid:layout_width="match_parent"android:layout_height="180dp"android:gravity="center"android:textSize="30sp"android:id="@+id/text"/><ImageViewandroid:layout_width="200dp"android:layout_height="200dp"android:layout_gravity="center_horizontal"android:id="@+id/image"/></LinearLayout>
这样轮播图ui方面基本就做好了,可以看到我们只是使用了一个ViewFlipper和对应要轮播的布局文件。下面是对应的活动的代码
class MainActivity : AppCompatActivity() {val bannerList = mutableListOf<BannerData>()lateinit var viewFlipper : ViewFlipperoverride fun onCreate(savedInstanceState: Bundle?) {super.onCreate(savedInstanceState)enableEdgeToEdge()setContentView(R.layout.activity_main)ViewCompat.setOnApplyWindowInsetsListener(findViewById(R.id.main)) { v, insets ->val systemBars = insets.getInsets(WindowInsetsCompat.Type.systemBars())v.setPadding(systemBars.left, systemBars.top, systemBars.right, systemBars.bottom)insets}viewFlipper = findViewById<ViewFlipper>(R.id.viewflipper)viewFlipper.setInAnimation(AnimationUtils.loadAnimation(this, android.R.anim.slide_out_right))viewFlipper.setInAnimation(AnimationUtils.loadAnimation(this, android.R.anim.slide_in_left))initBannerData()}fun initBannerData() {bannerList.add(BannerData("最爱的closer", R.drawable.closer))bannerList.add(BannerData("马孔多的炼金术士", R.drawable.bainiangudu))for (item in bannerList) {val itemView = createBanner(item)viewFlipper.addView(itemView)}startAutoFlipper()}private fun startAutoFlipper() {viewFlipper.flipInterval = 2000viewFlipper.startFlipping()}fun createBanner(bannerData: BannerData): View {val inflater = LayoutInflater.from(this)val itemView = inflater.inflate(R.layout.flipper_banner, viewFlipper, false)val imageView = itemView.findViewById<ImageView>(R.id.image)val textView = itemView.findViewById<TextView>(R.id.text)imageView.setImageResource(bannerData.imagePath)textView.setText(bannerData.text)return itemView}}
首先初始化了一段数据作为轮播图的子项,后面设置将对应的布局逐个添加到flipper中,最后设置轮播间隔再进行startFlipping()即可完成轮播图。vieFLipper作为ViewAnimator的子类,内部封装了一些简单的过渡动画,比如代码上用的就是简单的平移动画。最后的效果如下。
viewFlipper.setInAnimation(AnimationUtils.loadAnimation(this, android.R.anim.slide_out_right))
viewFlipper.setInAnimation(AnimationUtils.loadAnimation(this, android.R.anim.slide_in_left))

缺陷
ViewFlipper的优点就在于简单易操作,但是它在性能和功能上相对于ViewPager2实现的轮播图都较基础,具体来说,它不支持手势滑动操作,如需手动切换需自行实现触摸事件监听,且滑动体验较为基础,更重要的是性能限制:所有子视图一次性加载,当图片数量多或资源大时易引发内存问题,因此除了简单的定时切换展示,一般更建议使用下面的ViewPager2实现轮播图
ViewPager2实现轮播图
ViewPager2基于RecyclerView构建,可以享受RecyclerView的性能优化和复用机制,因此极大地提高了轮播图的性能,具体来说,性能优化的核心优势在下面两点
- 轮播图的每个页面通过
ViewHolder管理,当页面滑出屏幕时,对应的ViewHolder会被缓存到「复用池」中,而非直接销毁。当新页面滑入屏幕时,优先从复用池获取缓存的ViewHolder,仅更新数据(如图片资源、文本内容),避免重复执行inflate布局(耗时操作)和创建 View 对象。 - 只有当页面即将进入屏幕时(通过
onBindViewHolder),才会触发数据加载(如网络图片请求、数据绑定),避免一次性加载所有轮播项的数据。
同时,RecyclerVIew内部触摸机制也更加高效,可以帮助我们更简洁地实现一些用户触摸滑动的交互(例如触摸或者手动滑动时停止轮播)。缺点就是ViewPager2的轮播图实现比较繁琐,我们以一个商城app中常见的轮播图为例,下面详细看一下。

首先定义对应的布局文件,里面放入VIewPager2
<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android"xmlns:app="http://schemas.android.com/apk/res-auto"><data></data><androidx.constraintlayout.widget.ConstraintLayoutandroid:layout_width="match_parent"android:background="@color/white"android:layout_height="wrap_content"><androidx.viewpager2.widget.ViewPager2android:background="@color/white"android:layout_width="match_parent"android:layout_height="55dp"android:id="@+id/mine_viewpager2"/></androidx.constraintlayout.widget.ConstraintLayout>
</layout>
下面定义ViewPager2所需的文件
<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android"xmlns:app="http://schemas.android.com/apk/res-auto"><data></data><androidx.constraintlayout.widget.ConstraintLayoutandroid:layout_width="match_parent"android:padding="2dp"android:layout_height="match_parent"><Viewandroid:id="@+id/decor"android:layout_width="20dp"android:layout_height="0dp"android:background="@color/yellow"app:layout_constraintTop_toTopOf="@id/tips"app:layout_constraintBottom_toBottomOf="@id/tips"app:layout_constraintStart_toStartOf="parent"android:layout_marginStart="26dp" /><com.example.mybusyfish.CustomFontTextViewandroid:id="@+id/tips"android:layout_width="wrap_content"android:layout_height="wrap_content"android:text="TIPS"android:textSize="18sp"app:layout_constraintTop_toTopOf="parent"app:layout_constraintBottom_toBottomOf="parent"app:layout_constraintEnd_toEndOf="@id/decor"android:layout_margin="5dp" /><TextViewandroid:layout_width="wrap_content"android:layout_height="wrap_content"android:text="mine_content"android:id="@+id/mine_content"android:textSize="10sp"app:layout_constraintTop_toTopOf="@id/tips"app:layout_constraintBottom_toBottomOf="@id/tips"app:layout_constraintStart_toEndOf="@id/tips"android:layout_marginStart="11dp" /><ImageViewandroid:id="@+id/right_arrow"android:layout_width="wrap_content"android:layout_height="wrap_content"android:src="@drawable/right_arrow_black"app:layout_constraintTop_toTopOf="@id/tips"app:layout_constraintBottom_toBottomOf="@id/tips"app:layout_constraintEnd_toEndOf="parent"android:layout_marginEnd="8dp" /><ImageViewandroid:id="@+id/ic_tips"android:layout_width="27dp"android:layout_height="27dp"app:layout_constraintTop_toTopOf="@id/tips"app:layout_constraintBottom_toBottomOf="@id/tips"app:layout_constraintEnd_toStartOf="@id/right_arrow"android:layout_marginEnd="8dp"/></androidx.constraintlayout.widget.ConstraintLayout>
</layout>
这个item文件中“Tips”,“完善信息”以及后面的小图片都是要进行动态替换的。因此下面还要定义一个数据类
data class MyBanner (val name: String, val text: String, val path: Int)
既然是用ViewPager2,下面先写一个适配器。我们在Adapter的getItemCount()方法中返回一个极大的整数(如Integer.MAX_VALUE),并通过取模运算来决定每个位置显示哪张图片。
class MineBannerAdapter(private val list: List<MyBanner>) : RecyclerView.Adapter<MineBannerAdapter.ViewHolder>() {class ViewHolder(val binding: MineLunboBinding) : RecyclerView.ViewHolder(binding.root) {}override fun onCreateViewHolder(parent: ViewGroup,viewType: Int): MineBannerAdapter.ViewHolder {val binding = MineLunboBinding.inflate(LayoutInflater.from(parent.context), parent, false)return ViewHolder(binding)}override fun onBindViewHolder(holder: MineBannerAdapter.ViewHolder, position1: Int) {var position = position1 % list.sizeholder.binding.tips.text = list[position].nameholder.binding.mineContent.text = list[position].textholder.binding.icTips.setImageResource(list[position].path)}override fun getItemCount(): Int {return Int.MAX_VALUE}
}
下面是活动中的代码,因为ViewPager2其实并没有内置轮播图,因此需要用代码逻辑实现定时播放,可以用Handler的延时机制或者Timer实现定时。Handler自动在主线程执行。下面就用Handler演示一下。
class MainActivity : AppCompatActivity() {private lateinit var binding: ActivityMainBindingprivate lateinit var bannerAdapter: MineBannerAdapterprivate lateinit var handler: Handlerprivate val bannerList = listOf(MyBanner("TIPS", "你的个人信息待完善", R.drawable.bianji),MyBanner("TIPS", "淘宝买的宝贝看看还值多少钱", R.drawable.taobao),MyBanner("上新", "你有宝贝落灰啦,快翻新一下卖的更快", R.drawable.gouwu))companion object {private const val AUTO_SCROLL_DELAY = 3000Lprivate const val INITIAL_POSITION = 1000private const val RESUME_DELAY = 3000L // 用户交互后延迟3秒再恢复自动轮播}override fun onCreate(savedInstanceState: Bundle?) {super.onCreate(savedInstanceState)binding = DataBindingUtil.setContentView(this, R.layout.activity_main)initDataBindingComponents()setupViewPager()startAutoScroll()}private fun initDataBindingComponents() {handler = Handler(Looper.getMainLooper())}private fun setupViewPager() {bannerAdapter = MineBannerAdapter(bannerList)binding.viewPager2.adapter = bannerAdapterbinding.viewPager2.setCurrentItem(INITIAL_POSITION, false)binding.viewPager2.registerOnPageChangeCallback(object : ViewPager2.OnPageChangeCallback() {override fun onPageScrollStateChanged(state: Int) {handleScrollStateChange(state)}override fun onPageSelected(position: Int) {}})binding.viewPager2.setOnTouchListener { _, event ->when (event.action) {MotionEvent.ACTION_DOWN, MotionEvent.ACTION_MOVE -> {stopAutoScroll()}MotionEvent.ACTION_UP, MotionEvent.ACTION_CANCEL -> {// 触摸结束后延迟恢复自动轮播handler.postDelayed({startAutoScroll()}, RESUME_DELAY)}}false}}private fun handleScrollStateChange(state: Int) {when (state) {ViewPager2.SCROLL_STATE_DRAGGING -> {stopAutoScroll()}ViewPager2.SCROLL_STATE_SETTLING -> {stopAutoScroll()}ViewPager2.SCROLL_STATE_IDLE -> {// 滑动完全停止后,延迟恢复自动轮播handler.postDelayed({startAutoScroll()}, RESUME_DELAY)}}}private val autoScrollRunnable = object : Runnable {override fun run() {val currentItem = binding.viewPager2.currentItembinding.viewPager2.setCurrentItem(currentItem + 1, true)handler.postDelayed(this, AUTO_SCROLL_DELAY)}}private fun startAutoScroll() {stopAutoScroll()handler.postDelayed(autoScrollRunnable, AUTO_SCROLL_DELAY)}private fun stopAutoScroll() {handler.removeCallbacks(autoScrollRunnable)}override fun onResume() {super.onResume()startAutoScroll()}override fun onPause() {super.onPause()stopAutoScroll()}override fun onDestroy() {super.onDestroy()handler.removeCallbacks(autoScrollRunnable)}
}
上面代码比较多,算是ViewPager2实现轮播图的缺点,我们重点看一下核心代码
private val autoScrollRunnable = object : Runnable {override fun run() {val currentItem = binding.viewPager2.currentItembinding.viewPager2.setCurrentItem(currentItem + 1, true)handler.postDelayed(this, AUTO_SCROLL_DELAY)}}private fun startAutoScroll() {stopAutoScroll()handler.postDelayed(autoScrollRunnable, AUTO_SCROLL_DELAY)
}private fun stopAutoScroll() {handler.removeCallbacks(autoScrollRunnable)
}
这里先通过Runnable定义了延迟执行的事件,即ViewPager2的翻页动作,该事件会进行递归调用一直轮询下去。然后调用postDelayed()方法进行延迟播放,该方法第一个参数是要执行的事件,第二个参数是延迟的时间,上面定义该常量为3秒。
另外一开始定义了private const val INITIAL_POSITION = 1000并调用binding.viewPager2.setCurrentItem(INITIAL_POSITION, false),将目前页数设置为1000,这是保证用户能向前滑动。如果一开始从0开始,轮播图可以正常轮播,但是用户如果想要向前滑动就做不到了。
再看三个生命周期的回调方法中,onResume中执行startAutoScroll()是为了确保用户离开该界面后重新返回界面可以继续开始轮播,
onPause中停止则是离开界面、该app进入后台时暂停轮播图以免耗电,onDestroy中调用removeCallbacks是因为,如果Handler中有未处理的延迟消息或Runnable,即使Activity已经被销毁,消息队列仍然持有Handler的引用,而Handler又持有Activity的引用,导致Activity无法被垃圾回收器回收,从而会造成内存泄漏。
override fun onResume() {super.onResume()startAutoScroll()
}override fun onPause() {super.onPause()stopAutoScroll()
}override fun onDestroy() {super.onDestroy()handler.removeCallbacks(autoScrollRunnable)
}
另外的代码则是对用户交互进行处理,当用户点击或者拖动时停止轮播,当用户点击或者拖动时继续轮播。
binding.viewPager2.registerOnPageChangeCallback(object : ViewPager2.OnPageChangeCallback() {override fun onPageScrollStateChanged(state: Int) {handleScrollStateChange(state)}override fun onPageSelected(position: Int) {}})binding.viewPager2.setOnTouchListener { _, event ->when (event.action) {//用户点下,或者移动时停止MotionEvent.ACTION_DOWN, MotionEvent.ACTION_MOVE -> {stopAutoScroll()}MotionEvent.ACTION_UP, MotionEvent.ACTION_CANCEL -> {// 触摸结束后延迟恢复自动轮播handler.postDelayed({startAutoScroll()}, RESUME_DELAY)}}false}private fun handleScrollStateChange(state: Int) {when (state) {//用户进行拖动时停止ViewPager2.SCROLL_STATE_DRAGGING -> {stopAutoScroll()}ViewPager2.SCROLL_STATE_SETTLING -> {stopAutoScroll()}ViewPager2.SCROLL_STATE_IDLE -> {// 滑动完全停止后,延迟恢复自动轮播handler.postDelayed({startAutoScroll()}, RESUME_DELAY)}}}
做好这些工作后,轮播图就能正常工作了。我们就得到了一个更高性能的轮播图了
总结
ViewPager2基于RecyclerVIew有着更好的性能,尽管在使用上对开发者来说较ViewFlipper更为繁琐,但是这对于app的性能是值得的。除了少数情况下轮播内容仅限于极简单的内容,更多时候仍然推荐以ViewPage2的形式实现轮播图
