手机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的形式实现轮播图

相关推荐
00后程序员张3 小时前
iOS 26 开发者工具推荐,构建高效调试与性能优化工作流
android·ios·性能优化·小程序·uni-app·iphone·webview
小范馆5 小时前
通过 useEventBus 和 useEventCallBack 实现与原生 Android、鸿蒙、iOS 的事件交互
android·ios·harmonyos
恋猫de小郭5 小时前
Flutter 也有类 React Flow 的节点流程编辑器,快来了解下刚刚开源的 vyuh_node_flow
android·前端·flutter
2501_916008895 小时前
iOS 26 文件导出与数据分析,多工具组合下的开发者实践指南
android·macos·ios·小程序·uni-app·cocoa·iphone
2501_916008895 小时前
iOS混淆实战用多工具组合把IPA加固做成可复用的工程能力(iOS混淆 IPA加固 无源码混淆
android·ios·小程序·https·uni-app·iphone·webview
wangdaoyin20105 小时前
UniApp 在手机端(Android)打开选择文件和文件写入
android·前端·uni-app
咕噜企业签名分发-淼淼5 小时前
app分发平台哪个好点?手机app应用内测分发平台支持负载均衡的重要性
运维·智能手机·负载均衡
我命由我123456 小时前
Android PDF 操作 - AndroidPdfViewer 显示 PDF 异常清单(数据为 null、数据为空、PDF 文件损坏、非 PDF 文件)
android·java·java-ee·pdf·android studio·android-studio·android runtime
zhilin_tang7 小时前
揭开Linux跨平台 adb调试原理神秘面纱
android·linux