手机App上的轮播图是如何实现的—探究安卓轮播图

安卓开发轮播图

安卓原生开发实现轮播图一般有两种方法,一种是直接使用ViewFlipper组件,另一种是基于ViewPager2实现轮播图。本文将对这两种方法进行讲解。ViewFlipper相较于ViewPager2用法比较简单,但是性能和效果相较ViewPager2就相对差一些,下面先来看ViewFlipper

ViewFlipper

ViewFlipper继承自VIewAnimator,而ViewAnimator继承自FrameLayout,是一个ViewGroup。它是安卓系统提供的原生ui组件,主要用于在多个子视图中实现带有动画效果的切换,常用于制作轮播图。作为一个ui组件,ViewFlipper的使用是比较简单的。

如果只需要轮播图片或者其他单个ui控件的话可以直接将imageview或控件放进ViewFlipper中,下面我们展示对布局文件进行轮播的例子。首先在布局中写一个ViewFlipper

xml 复制代码
<?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"
    xmlns:tools="http://schemas.android.com/tools"
    android:id="@+id/main"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context=".MainActivity">
    <ViewFlipper
        android:id="@+id/viewflipper"
        android:layout_width="match_parent"
        android:layout_height="match_parent"/>
</androidx.constraintlayout.widget.ConstraintLayout>

写完ViewFlipper后再写对应的子项布局文件,下面以一个TextView和ImageView的简单组合为例

xml 复制代码
<?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">

    <TextView
        android:layout_width="match_parent"
        android:layout_height="180dp"
        android:gravity="center"
        android:textSize="30sp"
        android:id="@+id/text"/>

    <ImageView
        android:layout_width="200dp"
        android:layout_height="200dp"
        android:layout_gravity="center_horizontal"
        android:id="@+id/image"/>

</LinearLayout>

这样轮播图ui方面基本就做好了,可以看到我们只是使用了一个ViewFlipper和对应要轮播的布局文件。下面是对应的活动的代码

kotlin 复制代码
class MainActivity : AppCompatActivity() {
    val bannerList = mutableListOf<BannerData>()
    lateinit var viewFlipper : ViewFlipper
    override 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 = 2000
        viewFlipper.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的子类,内部封装了一些简单的过渡动画,比如代码上用的就是简单的平移动画。最后的效果如下。

kotlin 复制代码
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 复制代码
<?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.ConstraintLayout
        android:layout_width="match_parent"
        android:background="@color/white"
        android:layout_height="wrap_content">
        
         <androidx.viewpager2.widget.ViewPager2
              android:background="@color/white"
              android:layout_width="match_parent"
              android:layout_height="55dp"
              android:id="@+id/mine_viewpager2"/>
        
    </androidx.constraintlayout.widget.ConstraintLayout>
</layout>

下面定义ViewPager2所需的文件

xml 复制代码
<?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.ConstraintLayout
        android:layout_width="match_parent"
        android:padding="2dp"
        android:layout_height="match_parent">

        <View
            android: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.CustomFontTextView
            android: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" />

        <TextView
            android: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" />

        <ImageView
            android: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" />

        <ImageView
            android: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","完善信息"以及后面的小图片都是要进行动态替换的。因此下面还要定义一个数据类

kotlin 复制代码
data class MyBanner (val name: String, val text: String, val path: Int)

既然是用ViewPager2,下面先写一个适配器。我们在Adapter的getItemCount()方法中返回一个极大的整数(如Integer.MAX_VALUE),并通过取模运算来决定每个位置显示哪张图片。

kotlin 复制代码
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.size
        holder.binding.tips.text = list[position].name
        holder.binding.mineContent.text = list[position].text
        holder.binding.icTips.setImageResource(list[position].path)
    }

    override fun getItemCount(): Int {
        return Int.MAX_VALUE
    }
}

下面是活动中的代码,因为ViewPager2其实并没有内置轮播图,因此需要用代码逻辑实现定时播放,可以用Handler的延时机制或者Timer实现定时。Handler自动在主线程执行。下面就用Handler演示一下。

kotlin 复制代码
class MainActivity : AppCompatActivity() {
    
    private lateinit var binding: ActivityMainBinding
    private lateinit var bannerAdapter: MineBannerAdapter
    private lateinit var handler: Handler
    
    private 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 = 3000L
        private const val INITIAL_POSITION = 1000
        private 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 = bannerAdapter
        binding.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.currentItem
            binding.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实现轮播图的缺点,我们重点看一下核心代码

kotlin 复制代码
private val autoScrollRunnable = object : Runnable {
        override fun run() {
            val currentItem = binding.viewPager2.currentItem
            binding.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无法被垃圾回收器回收,从而会造成内存泄漏。

kotlin 复制代码
override fun onResume() {
    super.onResume()
    startAutoScroll()
}

override fun onPause() {
    super.onPause()
    stopAutoScroll()
}

override fun onDestroy() {
    super.onDestroy()
    handler.removeCallbacks(autoScrollRunnable)
}

另外的代码则是对用户交互进行处理,当用户点击或者拖动时停止轮播,当用户点击或者拖动时继续轮播。

kotlin 复制代码
       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的形式实现轮播图

相关推荐
柯南二号5 分钟前
【大前端】【Android】把 Activity 重构成 MVVM 的对比示例
android·状态模式
某空m18 分钟前
【Android】Glide的缓存机制
android·缓存·glide
某空m27 分钟前
【Android】Glide的使用
android·glide
QING61828 分钟前
Jetpack Compose 中的 ViewModel 作用域管理 —— 新手指南
android·kotlin·android jetpack
鹏多多35 分钟前
flutter-使用EventBus实现组件间数据通信
android·前端·flutter
TheNextByte11 小时前
如何轻松地将联想手机中的数据传输到电脑
智能手机·电脑
ShayneLee81 小时前
Nginx修改请求头响应头
android·运维·nginx
廋到被风吹走1 小时前
【数据库】【MySQL】高可用与扩展方案深度解析
android·数据库·mysql
恋猫de小郭1 小时前
Flutter 官方正式解决 WebView 在 iOS 26 上有点击问题
android·前端·flutter
CaspianSea5 小时前
编译Android 16 TV模拟器(一)
android