企业🤖AI智能体构建引擎,智能编排和调试,一键部署,支持私有化部署方案 广告
> 对应项目:`carousel` [TOC] # 1. 前言 也就是十分常见的轮播图,可以自动播放,有对应指示点和标题,可以无限播放。 # 2. 实现 ## 2.1 基础ViewPager 首先定义一个布局文件: ~~~ <?xml version="1.0" encoding="utf-8"?> <RelativeLayout 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:layout_width="match_parent" android:layout_height="match_parent" tools:context=".MainActivity"> <androidx.viewpager.widget.ViewPager android:id="@+id/viewPager" android:layout_width="match_parent" android:layout_height="200dp" /> <LinearLayout android:layout_width="match_parent" android:orientation="vertical" android:background="#33000000" android:gravity="center" android:layout_alignBottom="@id/viewPager" android:layout_height="wrap_content"> <TextView android:id="@+id/textView_title" android:text="@string/app_name" android:textColor="@color/white" android:textSize="22sp" android:padding="4dp" android:layout_width="wrap_content" android:layout_height="wrap_content"/> <LinearLayout android:id="@+id/linearLayout_pointer" android:orientation="horizontal" android:layout_width="wrap_content" android:layout_height="wrap_content" /> </LinearLayout> </RelativeLayout> ~~~ 然后完成数据的装载: ~~~ val imageDrawableId = listOf<Int>( R.drawable.a, R.drawable.b, R.drawable.c, R.drawable.d, R.drawable.e ) val imageViewList = mutableListOf<ImageView>() fun initialization(){ for (imageId in imageDrawableId) { val imageView = ImageView(this) imageView.setBackgroundResource(imageId) imageViewList.add(imageView) } } ~~~ 调用`initialization`方法之后,就可以为`ViewPager`设置一个适配器: ~~~ inner class MyViewPagerAdapter: PagerAdapter() { // 初始化 container->ViewPager override fun instantiateItem(container: ViewGroup, position: Int): Any { val currentPosition = (position % imageViewList.size) val view = imageViewList[currentPosition] if(view.parent != null){ (view.parent as ViewGroup).removeView(view) } container.addView(view) return view } // 返回图片个数 override fun getCount(): Int { return imageViewList.size } override fun isViewFromObject(view: View, `object`: Any): Boolean { return view == `object` } // 移除 override fun destroyItem(container: ViewGroup, position: Int, `object`: Any) { container.removeView(`object` as View) } } ~~~ 最后为找到的`ViewPager`示例设置适配器即可: ~~~ viewPager.adapter = MyViewPagerAdapter() ~~~ 这样可以简单做到简单使用。但是在文章开头所说的无限轮播、指示器、标题等均还没有实现。下面就继续在其上添加功能。 ## 2.2 添加指示器和标题 为了添加指示器,我们可以用`ImageView`来替代每一个指示器的点。这里在初始化方法中进行添加: ~~~ fun initialization(){ for (imageId in imageDrawableId) { val imageView = ImageView(this) imageView.setBackgroundResource(imageId) imageViewList.add(imageView) } // 添加指示器 for(i in 0.until(imageDrawableId.size)){ val textView = ImageView(this) textView.setBackgroundResource(R.drawable.pointer) val layoutParams = LinearLayout.LayoutParams(20, 20) if(i == 0){ Log.e("TAG", "isEnable=true: " ) textView.isEnabled = true } else{ textView.isEnabled = false layoutParams.marginStart = 8 } textView.layoutParams = layoutParams linearLayout_pointer.addView(textView) } } ~~~ 对于设置的`pointer.xml`文件设置为: ~~~ // res/drawable/pointer.xml <?xml version="1.0" encoding="utf-8"?> <selector xmlns:android="http://schemas.android.com/apk/res/android"> <item android:state_enabled="false" android:drawable="@drawable/pointer_normal"/> <item android:state_enabled="true" android:drawable="@drawable/pointer_pressed"/> </selector> ~~~ 然后就是指定背景颜色的两个文件: ~~~ // res/drawable/pointer_normal.xml <?xml version="1.0" encoding="utf-8"?> <shape xmlns:android="http://schemas.android.com/apk/res/android" android:shape="oval"> <solid android:color="@color/gray" /> <size android:width="8dp" android:height="8dp" /> </shape> ~~~ 至于`pointer_pressed.xml`文件就只是颜色不同,这里不再给出。 然后就可以看到静态的指示器: ![](https://img.kancloud.cn/07/44/07448e55c0f682ce4dd8a19550b8ca9f_554x108.png) 为了实现动态效果,我们这里可以使用`ViewPager`的监听器来监听滑动,然后对应的来改变`ImageView`的样式: ~~~ viewPager.addOnPageChangeListener(object : ViewPager.OnPageChangeListener{ override fun onPageScrolled( position: Int, positionOffset: Float, positionOffsetPixels: Int ) { } override fun onPageSelected(position: Int) { // 这里关联指示器的动态效果 linearLayout_pointer.getChildAt(position).isEnabled = true // 之前的设置为灰色 linearLayout_pointer.getChildAt(currentPointerIndex).isEnabled = false // 更新当前下标 currentPointerIndex = position // 更新标题 textView_title.setText(textViewTitle[position]) } override fun onPageScrollStateChanged(state: Int) { } }) ~~~ 设置后就完成关联指示器和标题了。现在的任务就是最后一个,也就是无限轮播功能。 ## 2.3 添加无限轮播功能 可以将任务拆解为两个部分,一个为无限循环,一个为自动轮播。首先我们需要解决无限循环问题。 ### 2.3.1 无限循环 对于我们这里的五张图片,我们期望在第一张的前面可以滑动到最后一张;同时,对于最后一张图片期望可以滑动到第一张。对于指示器,由于是我们自己所控制的,很容易就能做到这一点。这里的难点就在于`ViewPager`这个控件。不妨来做一个简单的测试,首先在布局文件下面添加一个按钮: ~~~ <Button android:id="@+id/btn" android:layout_below="@id/t" android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="按钮" android:layout_marginTop="10dp" android:layout_centerHorizontal="true" /> ~~~ 然后设置点击事件: ~~~ findViewById<Button>(R.id.btn).setOnClickListener { viewPager.currentItem += 1 } ~~~ 就可以发现当我们点击一次按钮,`ViewPager`就自动移动到下一张图片。但是到最后一张图片的时候不再移动。所以这里我们如果重新测试: ~~~ findViewById<Button>(R.id.btn).setOnClickListener { var temp = viewPager.currentItem + 1 temp %= imageViewList.size viewPager.currentItem = temp } ~~~ 这样虽然可以实现从最后一张图片到第一张图片,但是中间所产生的动画着实不美观。 ![](https://img.kancloud.cn/47/65/4765b12e7b840abd25a2741f9d570423_560x356.png) 也就是这里我们需要一个更加平滑的过渡效果。在网上有两种做法: (1)采用`Adapter`内的`getCount()`方法返回`Integer.MAX_VALUE`。 (2)在列表的最前面插入最后一条数据,在列表末尾插入第一个数据,造成循环的假象。 #### 第一种方法实现无限滑动 ~~~ class MainActivity : AppCompatActivity() { val viewPager by lazy { findViewById<ViewPager>(R.id.viewPager) } val textView_title by lazy { findViewById<TextView>(R.id.textView_title) } val linearLayout_pointer by lazy { findViewById<LinearLayout>(R.id.linearLayout_pointer) } val imageDrawableId = listOf<Int>( R.drawable.a, R.drawable.b, R.drawable.c, R.drawable.d, R.drawable.e ) val textViewTitle = listOf<String>( "尚硅谷波河争霸赛!", "凝聚你我,放飞梦想!", "抱歉没座位了!", "7月就业名单全部曝光!", "平均起薪11345元" ) var currentPointerIndex = 0 override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContentView(R.layout.activity_main) // 为recyclerView添加图片资源 initialization() currentPointerIndex = Int.MAX_VALUE/2 - Int.MAX_VALUE/2%(imageViewList.size) // 设置适配器 viewPager.adapter = MyViewPagerAdapter() // 设置从第一个开始 viewPager.currentItem = currentPointerIndex viewPager.addOnPageChangeListener(object : ViewPager.OnPageChangeListener { override fun onPageScrolled( position: Int, positionOffset: Float, positionOffsetPixels: Int ) { } override fun onPageSelected(position: Int) { // 这里需要对应更新指示器和标题的下标 updatePointer(position) } override fun onPageScrollStateChanged(state: Int) { } }) } fun updatePointer(position: Int) { Log.e("TAG", "updatePointer: ${position}" ) // 这里关联指示器的动态效果 linearLayout_pointer.getChildAt(position%(imageViewList.size)).isEnabled = true // 之前的设置为灰色 linearLayout_pointer.getChildAt(currentPointerIndex%(imageViewList.size)).isEnabled = false // 更新标题 textView_title.setText(textViewTitle[currentPointerIndex%(imageViewList.size)]) // 更新当前下标 currentPointerIndex = position } val imageViewList = mutableListOf<ImageView>() fun initialization() { // DABCDEA for (imageId in imageDrawableId) { val imageView = ImageView(this) imageView.setBackgroundResource(imageId) imageViewList.add(imageView) } // 添加指示器 for (i in 0.until(imageDrawableId.size)) { val imageView = ImageView(this) imageView.setBackgroundResource(R.drawable.pointer) val layoutParams = LinearLayout.LayoutParams(20, 20) if (i == 0) { imageView.isEnabled = true } else { imageView.isEnabled = false layoutParams.marginStart = 8 } imageView.layoutParams = layoutParams linearLayout_pointer.addView(imageView) } // 设置标题 textView_title.setText(textViewTitle[currentPointerIndex%(imageViewList.size)]) } inner class MyViewPagerAdapter : PagerAdapter() { // 初始化 container->ViewPager override fun instantiateItem(container: ViewGroup, position: Int): Any { val view = imageViewList[position%(imageViewList.size)] if (view.parent != null) { (view.parent as ViewGroup).removeView(view) } container.addView(view) return view } // 返回图片个数 override fun getCount(): Int { return Int.MAX_VALUE } override fun isViewFromObject(view: View, `object`: Any): Boolean { return view == `object` } // 移除 override fun destroyItem(container: ViewGroup, position: Int, `object`: Any) { container.removeView(`object` as View) } } } ~~~ 这里需要注意的是,所有的`position`或者`currentPointerIndex`都需要进行取模运算。其中,设置`currentPointerIndex = Int.MAX_VALUE/2 - Int.MAX_VALUE/2%(imageViewList.size)`就是为了保证为第一个下标,即0的倍数。但是,设置这么大的数,在使用`setCurrentItem()`的时候会发生`ANR`,比如下面的测试案例: ~~~ findViewById<Button>(R.id.btn).setOnClickListener { val temp = viewPager.currentItem + 1 // 判断temp的位置 viewPager.currentItem = temp%(imageViewList.size) } ~~~ 所以还是设置一个适中的值比较好。 #### 第二种方法实现无限滑动 比如我们这里放置的图片为`ABCDE`,那么可以在第一张前面放置最后一张,在最后一张后面放置第一张。也就是:`EABCDEA`这么一个列表。 * 当滑动到最后一个`A`的时候,就将其设置当前页为下标`1`的`A`页面。 * 当滑动到第一个`E`的时候,同理切换到最后一个`E`页面; 至于为什么可以这么做呢,这里我们做一个简单的示范案例,还是对上面的监听函数做处理: ~~~ findViewById<Button>(R.id.btn).setOnClickListener { var temp = viewPager.currentItem + 1 temp %= imageViewList.size // 第二个参数为:boolean smoothScroll viewPager.setCurrentItem(temp, false) } ~~~ 这里传入`smoothScroll = false`表示不使用平滑过渡效果。也就是对于整体而言均为: ![](https://img.kancloud.cn/4b/f9/4bf90302ae509bca5f54793e07893073_556x424.png) 正是因为我们可以设置没有平滑过渡效果,所以这里才可以在前后插入,然后替换。 ~~~ class MainActivity : AppCompatActivity() { val viewPager by lazy { findViewById<ViewPager>(R.id.viewPager) } val imageDrawableId = listOf<Int>( R.drawable.e, R.drawable.a, R.drawable.b, R.drawable.c, R.drawable.d, R.drawable.e, R.drawable.a ) var currentPointerIndex = 0 override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContentView(R.layout.activity_main) // 为recyclerView添加图片资源 initialization() // 设置适配器 viewPager.adapter = MyViewPagerAdapter() // 设置从第一个开始 viewPager.currentItem = currentPointerIndex viewPager.addOnPageChangeListener(object : ViewPager.OnPageChangeListener { override fun onPageScrolled( position: Int, positionOffset: Float, positionOffsetPixels: Int ) { } override fun onPageSelected(position: Int) { currentPointerIndex = position } override fun onPageScrollStateChanged(state: Int) { //验证当前的滑动是否结束 if (state == ViewPager.SCROLL_STATE_IDLE) { if (currentPointerIndex == 0) { viewPager.setCurrentItem(imageViewList.size - 2, false);//切换,不要动画效果 } else if (currentPointerIndex == imageViewList.size - 1) { viewPager.setCurrentItem(1, false);//切换,不要动画效果 } } } }) } val imageViewList = mutableListOf<ImageView>() fun initialization() { // EABCDEA for (imageId in imageDrawableId) { val imageView = ImageView(this) imageView.setBackgroundResource(imageId) imageViewList.add(imageView) } } inner class MyViewPagerAdapter : PagerAdapter() { // 初始化 container->ViewPager override fun instantiateItem(container: ViewGroup, position: Int): Any { val view = imageViewList[position] if (view.parent != null) { (view.parent as ViewGroup).removeView(view) } container.addView(view) return view } // 返回图片个数 override fun getCount(): Int { return imageViewList.size } override fun isViewFromObject(view: View, `object`: Any): Boolean { return view == `object` } // 移除 override fun destroyItem(container: ViewGroup, position: Int, `object`: Any) { container.removeView(`object` as View) } } } ~~~ 为了简化一下,这里将指示器和文字都去掉了。 ### 2.3.2 自动轮播 前面我们知道指定`viewPager.currentItem`属性可以做到切换,所以这里只需要隔一段事件就调用一下这个方法就可以实现自动轮播。那么就可以使用`Handler`来延迟发送一条消息。 ~~~ handler = object: Handler(mainLooper){ override fun handleMessage(msg: Message) { super.handleMessage(msg) if(msg.what == 0){ // 设置下一页 val item = (viewPager.currentItem + 1) % imageViewList.size viewPager.currentItem = item // 延迟发送消息,一直回调自己 sendEmptyMessageDelayed(0, 4000) } } } ~~~ 然后调用启动这个`Handler`: ~~~ // 启动自动Handler handler!!.sendEmptyMessage(0) ~~~ 就可以做到自动轮播的效果。接下来需要完成点击某张图片后暂停当前的轮播。 ### 2.3.3 暂停轮播 需要暂停轮播就需要对用户的触摸事件进行处理。所以这里可以为每个`ImageView`注册触摸事件: ~~~ override fun instantiateItem(container: ViewGroup, position: Int): Any { val view = imageViewList[position] // 注册触摸事件 view.setOnTouchListener(object : View.OnTouchListener { override fun onTouch(v: View?, event: MotionEvent?): Boolean { when(event?.action){ MotionEvent.ACTION_DOWN -> { // 按下 // 按下需要暂停,所以需要清空消息队列中的所有消息 handler?.removeCallbacksAndMessages(null) // 传入null,表示清空所有消息 } MotionEvent.ACTION_UP -> { // 离开 // 离开,也就是再次发送一个延迟的消息到消息队列中 handler?.removeCallbacksAndMessages(null) // 清空可能存在的消息 handler?.sendEmptyMessageDelayed(0, 4000) } } return true; // 表示消费掉事件 } }) if (view.parent != null) { (view.parent as ViewGroup).removeView(view) } container.addView(view) return view } ~~~ 这里虽然可以实现按下清空消息也就是暂停轮播,释放重新开始轮播。但是没有处理用户滑动/拖拽事件,也就是说在用户拖拽的时候,由于我们在按下后没有做处理,所以这里会有`bug`。也即是:拖拽后再释放就没有轮播的效果了。 为了解决这个问题,这里可以监听`ViewPager`的三种状态,在`ViewPager`的三种状态分别为: * `ViewPager.SCROLL_STATE_DRAGGING` : 当用户按下`ViewPager`视图并且需要滑动第一下时; * ` ViewPager.SCROLL_STATE_SETTLING`: 当用户滑动的放手让其惯性滑动的时候,比如滑了放手触发。如果用户滑了左手边一点然后不松手滑回原点将不会触发 * `ViewPager.SCROLL_STATE_IDLE` : 当用户滑动的时候松手 所以为了实现用户拖拽后还可以继续,这里需要对拖拽进行处理。在用户拖拽的时候,清空消息队列,在用户松手的时候继续发送消息。即: ~~~ // 为viewpager注册一个监听器,用来监听其状态发生改变的时候 viewPager.addOnPageChangeListener(object : ViewPager.OnPageChangeListener{ override fun onPageScrolled( position: Int, positionOffset: Float, positionOffsetPixels: Int ) { } override fun onPageSelected(position: Int) { } // viewpager状态发生改变 override fun onPageScrollStateChanged(state: Int) { when(state){ ViewPager.SCROLL_STATE_DRAGGING -> { // 开始拖拽的时候 handler?.removeCallbacksAndMessages(null) } ViewPager.SCROLL_STATE_IDLE -> { // 滑动释放的时候 handler?.removeCallbacksAndMessages(null) handler?.sendEmptyMessageDelayed(0, 4000) } } } }) ~~~ ## 2.4 完整版 ~~~ /** * 完整版,包括指示器和标题,自动轮播,点击暂停等 */ class MainActivity : AppCompatActivity() { val viewPager by lazy { findViewById<ViewPager>(R.id.other_viewpager) } val other_linearlayout by lazy { findViewById<LinearLayout>(R.id.other_linearlayout) } val other_textview by lazy { findViewById<TextView>(R.id.other_textview) } val imageDrawableId = listOf<Int>( R.drawable.e, R.drawable.a, R.drawable.b, R.drawable.c, R.drawable.d, R.drawable.e, R.drawable.a ) var currentPointerIndex = 0 var handler: Handler? = null /** * 因为采用图片前后加一个,所以实际上和指示器的对应关系需要计算 */ fun mappingImageIndexToLogisticIndex(index: Int): Int{ if(index > 0 && index < imageViewList.size - 1){ return index - 1 } else if( index == 0) { return imageViewList.size - 3 } else { return 0 } } /** * toNext: 表示是否是用户手动触发的滚动 * right: 表示滑动的方向是否是向右 */ fun updateInfo(toNext: Boolean, right: Boolean){ // 计算下一页下标 var item = 0 item = if(right) (currentPointerIndex + 1) % imageViewList.size else if (currentPointerIndex - 1 < 0) imageViewList.size - 3 else ((currentPointerIndex - 1) % imageViewList.size) if(toNext) viewPager.currentItem = item // 切换指示器 for(i in 0.until(textViewTitle.size)){ other_linearlayout.getChildAt(i)?.isEnabled = false } other_linearlayout.getChildAt(mappingImageIndexToLogisticIndex(item))?.isEnabled = true // 设置标题 other_textview.text = textViewTitle[mappingImageIndexToLogisticIndex(item)] } override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContentView(R.layout.activity_other) // 为recyclerView添加图片资源 initialization() handler = object: Handler(mainLooper){ override fun handleMessage(msg: Message) { super.handleMessage(msg) if(msg.what == 0){ updateInfo(true, true) // 延迟发送消息,一直回调自己 sendEmptyMessageDelayed(0, 4000) } } } // 设置适配器 viewPager.adapter = MyViewPagerAdapter() // 设置从第一个开始 viewPager.currentItem = currentPointerIndex viewPager.addOnPageChangeListener(object : ViewPager.OnPageChangeListener { var currentPosition = 0 var left_direction = false override fun onPageScrolled( position: Int, positionOffset: Float, positionOffsetPixels: Int ) { left_direction = position <= currentPosition currentPosition = position } override fun onPageSelected(position: Int) { currentPointerIndex = position // 更新一下指示器和标题 updateInfo(false, !left_direction) } override fun onPageScrollStateChanged(state: Int) { //验证当前的滑动是否结束 if (state == ViewPager.SCROLL_STATE_IDLE) { if (currentPointerIndex == 0) { viewPager.setCurrentItem(imageViewList.size - 2, false);//切换,不要动画效果 } else if (currentPointerIndex == imageViewList.size - 1) { viewPager.setCurrentItem(1, false);//切换,不要动画效果 } } } }) // 为viewpager注册一个监听器,用来监听其状态发生改变的时候 viewPager.addOnPageChangeListener(object : ViewPager.OnPageChangeListener{ override fun onPageScrolled( position: Int, positionOffset: Float, positionOffsetPixels: Int ) { } override fun onPageSelected(position: Int) { } // viewpager状态发生改变 override fun onPageScrollStateChanged(state: Int) { when(state){ ViewPager.SCROLL_STATE_DRAGGING -> { // 开始拖拽的时候 handler?.removeCallbacksAndMessages(null) } ViewPager.SCROLL_STATE_IDLE -> { // 滑动释放的时候 handler?.removeCallbacksAndMessages(null) handler?.sendEmptyMessageDelayed(0, 4000) } } } }) // 启动自动Handler handler!!.sendEmptyMessage(0) } val textViewTitle = listOf<String>( "标题1!", "标题2!", "标题3!", "标题4!", "标题5", ) val imageViewList = mutableListOf<ImageView>() fun initialization() { // EABCDEA for (imageId in imageDrawableId) { val imageView = ImageView(this) imageView.setBackgroundResource(imageId) imageViewList.add(imageView) } // 初始化指示器 for(i in 0.until(textViewTitle.size)){ val imageView = ImageView(this) imageView.setBackgroundResource(R.drawable.pointer) // 因为外层为LinearLayout,所以这里为LinearLayout val layoutParams = LinearLayout.LayoutParams(20, 20) if(i == 0){ imageView.isEnabled = true } else{ imageView.isEnabled = false layoutParams.marginStart = 8 } imageView.layoutParams = layoutParams other_linearlayout.addView(imageView) } // 设置标题为第1个 other_textview.text = textViewTitle[0] } inner class MyViewPagerAdapter : PagerAdapter() { // 初始化 container->ViewPager override fun instantiateItem(container: ViewGroup, position: Int): Any { val view = imageViewList[position] // 注册触摸事件 view.setOnTouchListener(object : View.OnTouchListener { override fun onTouch(v: View?, event: MotionEvent?): Boolean { when(event?.action){ MotionEvent.ACTION_DOWN -> { // 按下 // 按下需要暂停,所以需要清空消息队列中的所有消息 handler?.removeCallbacksAndMessages(null) // 传入null,表示清空所有消息 } MotionEvent.ACTION_UP -> { // 离开 // 离开,也就是再次发送一个延迟的消息到消息队列中 handler?.removeCallbacksAndMessages(null) // 清空可能存在的消息 handler?.sendEmptyMessageDelayed(0, 4000) } } return true; // 表示消费掉事件 } }) if (view.parent != null) { (view.parent as ViewGroup).removeView(view) } container.addView(view) return view } // 返回图片个数 override fun getCount(): Int { return imageViewList.size } override fun isViewFromObject(view: View, `object`: Any): Boolean { return view == `object` } // 移除 override fun destroyItem(container: ViewGroup, position: Int, `object`: Any) { container.removeView(`object` as View) } } } ~~~