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

实现

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
<?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一起传进去:

1
2
3
4
5
6
7
8
9
10
11
12
//加载轮播图适配器
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:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
/**
* 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中加载不同的布局。