Skip to content

在写寒假考核时,主界面是由一个viewpager2的轮播图和RecyclerView的文章列表,但这样有个问题,当我们向上滑动列表时,轮播图并不会跟着滑动,这样非常影响视觉体验,之前的效果参考WanAndroidREADME的主页界面展示,那么我们应该如何解决这个问题呢?就是将轮播图也添加进RecyclerView里面。效果如下:

实现

先新建一个banner.xml布局,用于展示我们的轮播图:

xml
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    xmlns:app="http://schemas.android.com/apk/res-auto">

    <androidx.viewpager2.widget.ViewPager2
        android:id="@+id/home_viewpager2"
        android:layout_width="match_parent"
        android:layout_height="140dp"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toTopOf="parent" />

    <TextView
        android:id="@+id/tv_carousel_text"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:background="#80000000"
        android:padding="12dp"
        android:text="Banner Text"
        android:textColor="#F5F5F5"
        android:textSize="10dp"
        android:textStyle="bold"
        app:layout_constraintBottom_toBottomOf="@+id/home_viewpager2"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintStart_toStartOf="parent" />

    <com.generlas.winterexam.view.CarouselDot
        android:id="@+id/home_carouselDot"
        android:layout_width="80dp"
        android:layout_height="38dp"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintEnd_toEndOf="@+id/tv_carousel_text"
        app:layout_constraintTop_toTopOf="@+id/tv_carousel_text" />

</androidx.constraintlayout.widget.ConstraintLayout>

里面的一些其他具体布局内容(比如自定义的三个点)可以到我的WanAndroid项目里面去找。

然后在HomeFragment.kt里面,之前是接收到请求的数据后在Fragment里面直接展示,现在我们接收到后只需要存储进数组里然后一会儿随着Adapter一起传进去:

kotlin
//加载轮播图适配器
override fun createCarousel(carouselPassage: List<CarouselInfo>) {
    if (activity != null) {
        val mainActivity = activity as MainActivity
        mainActivity.runOnUiThread {
            //多添加两张,模拟最后一张到第一张
            finalCarouselPassage.add(0, carouselPassage[carouselPassage.size - 1])
            finalCarouselPassage.addAll(carouselPassage)
            finalCarouselPassage.add(carouselPassage.size + 1, carouselPassage[0])
        }
    }
}

这里添加两张的原因是为了实现无限轮播,具体思路可在网上查询。

然后我们写一个HomePassageAdapter:

kotlin
/**
 * description : TODO:home页的recyclerview
 * date : 2025/3/2 13:20
 */
class HomePassageAdapter(private val context: Context, private val carouselInfo: List<CarouselInfo>) :
    ListAdapter<PassageInfo, RecyclerView.ViewHolder>(ItemDiffCallback()) {

    private val TYPE_BANNER = 0
    private val TYPE_PASSAGE = 1

    lateinit var handler: Handler
    lateinit var runnable: Runnable

    inner class PassageViewHolder(view: View) : RecyclerView.ViewHolder(view) {
        val passageAuthor: TextView = view.findViewById(R.id.tv_card_author)
        val passageDate: TextView = view.findViewById(R.id.tv_card_date)
        val passageTitle: TextView = view.findViewById(R.id.tv_card_title)
        val passageChapterName: TextView = view.findViewById(R.id.tv_card_chapterName)
    }

    inner class BannerViewHolder(view: View): RecyclerView.ViewHolder(view) {
        var dotView : CarouselDot = view.findViewById(R.id.home_carouselDot)
        val carouselViewPager2: ViewPager2 = view.findViewById(R.id.home_viewpager2)
        val mTvCarouselTitle: TextView = view.findViewById(R.id.tv_carousel_text)

        init {
            autoCarousel()
            handCarousel()
        }

        //手动滑动时暂停自动滑动
        private fun handCarousel() {
            carouselViewPager2.registerOnPageChangeCallback(object : ViewPager2.OnPageChangeCallback() {
                override fun onPageScrollStateChanged(state: Int) {
                    super.onPageScrollStateChanged(state)
                    if (state == ViewPager2.SCROLL_STATE_DRAGGING) {
                        handler.removeCallbacks(runnable)
                    } else {
                        if (state == ViewPager2.SCROLL_STATE_IDLE) {
                            handler.removeCallbacks(runnable) //需把之前的线程关闭掉,否则线程越来越多,自动切换速度越快
                            handler.postDelayed(runnable, 3000)
                        }
                    }
                }

                //当滑动到最后一张(实际上展示为第一张的内容)时,立刻跳到第一张,滑动到第0张(展示为最后一张)时,同理
                override fun onPageSelected(position: Int) {
                    super.onPageSelected(position)
                    mTvCarouselTitle.text = carouselInfo[position].title
                    if (position == carouselInfo.size - 1) {
                        dotView.changeDots(0)
                        carouselViewPager2.setCurrentItem(1, false)

                    } else if (position == 0) {
                        dotView.changeDots(carouselInfo.size - 1)
                        carouselViewPager2.setCurrentItem(carouselInfo.size - 2, false)

                    } else {
                        dotView.changeDots(position - 1)
                    }
                }
            })
        }

        //自动轮播
        private fun autoCarousel() {
            handler = Handler(Looper.getMainLooper())
            runnable = object : Runnable {
                override fun run() {
                    val nextPassage = carouselViewPager2.currentItem + 1
                    if(nextPassage == carouselInfo.size - 1) {
                        carouselViewPager2.setCurrentItem(1,false)
                    }
                    else {
                        carouselViewPager2.currentItem = nextPassage
                    }
                    handler.postDelayed(this, 3000)
                }
            }
            handler.postDelayed(runnable, 3000)
        }
    }

    override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder {
        if(viewType == TYPE_PASSAGE) {
            val view =
                LayoutInflater.from(parent.context).inflate(R.layout.home_card_item, parent, false)
            return PassageViewHolder(view)
        } else {
            val view = LayoutInflater.from(parent.context).inflate(R.layout.banner, parent, false)
            return BannerViewHolder(view)
        }
    }

    override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) {
        val passage = getItem(position)
        when(holder) {
            is PassageViewHolder -> {
                if(passage.author != "") {
                    holder.passageAuthor.text = passage.author
                    holder.passageDate.text = passage.niceDate
                } else {
                    holder.passageAuthor.text = passage.shareUser
                    holder.passageDate.text = passage.niceShareDate
                }
                val trueText = Html.fromHtml(passage.title, Html.FROM_HTML_MODE_COMPACT)
                holder.passageTitle.text = trueText
                holder.passageChapterName.text = passage.chapterName

                //整个View的点击事件
                holder.itemView.setOnClickListener {
                    val intent = Intent(context, WebViewActivity::class.java)
                    intent.putExtra("url", passage.link)
                    context.startActivity(intent)
                }
            }

            is BannerViewHolder -> {
                val carouselAdapter = CarouselViewPager2Adapter(context, carouselInfo)
                holder.carouselViewPager2.adapter = carouselAdapter
                holder.carouselViewPager2.currentItem = 1
                holder.dotView.initDots(carouselInfo.size - 2, 0)
                holder.mTvCarouselTitle.text = carouselInfo[1].title
            }
        }
    }

    override fun getItemViewType(position: Int): Int {
        if(position == 0) {
            return TYPE_BANNER
        } else {
            return TYPE_PASSAGE
        }
    }

}

这个与原来基本的RecyclerView的区别,一是我们重写了一个getItemViewType()方法,当当前position == 0时,我们就要加载轮播图的布局,否则就加载文章数据的布局。然后在 onCreateViewHolder()中,对不同的类型加载不同的布局并返回单独的ViewHolder(),比如如果接受的viewTypeTYPE_BANNER,就加载banner的布局并进行banner控件的绑定,反之亦然。然后在onBindViewHolder()中,我们对当前holder类型进行判断,如果是PassageViewHolder就进行文章列表数据的加载,注意这里when()方法可以自动将类型进行转换,比如这里可以转成PassageViewHolder

注意这里关于轮播图的自动播放和手动播放逻辑,一定要写在BannerViewHolder()里面,用init{}代码块进行初始化加载。因为如果在onBindViewHolder()中写的话会导致,往下滑后再往上滑,当轮播图重新进入页面时,会再次调用onBindViewHolder()方法,就会添加很多线程,导致自动切换速度越来越快,而在BannerViewHolder中就只会初始创建时调用一次。

这样就能实现了在RecyclerView中加载不同的布局。