赞
踩
本节教程我们来实现云音乐的主页展示,实现的效果如下图所示:
本节内容您将学习到如下内容:
vlayout是RecyclerView的LayoutManager扩展库,VirtualLayoutManager这个类负责RecyclerView的UI布局。继承于RecyclerView.Adapter的VirtualLayoutAdapter则是配合VirtualLayoutManager的对应的适配类。
大概的架构图如下所示:
说明:这个架构中,大部分的内容vlayout以为为我们实现完成了,我们只需要在子Adapter中实现Layout和Data的绑定工作就可以了。
vlayout主要提供了以下一系列的布局:
LinearLayoutHelper和系统提供的线性布局类似,能设置
bgColor
—背景颜色,bgImg
—背景图片,diverHeight
—分隔线高度等
GridLayoutHelper 和系统提供的网格布局类似,能设置
spanCount
—一行有几列,itemCount
—总共多少个Item,vGap
— item间的垂直间距,hGap
— item间的水平间距,AutoExpand
—最后一行如果没有足够的列数,是否充满整行
FixLayoutHelper的位置是固定的,不会随着RecyclerView滚动而滚动,位置可以根据
alignType
(TOP_LEFT,TOP_RIGHT,BOTTOM_LEFT,BOTTOM_RIGHT)和X
,Y
值来确定。
ScrollFixLayoutHelper 和 FixLayoutHelper类似也是固定位置显示的,但是可以当滚动到一定的位置时候才显示,如果
showType
设置为SHOW_ALWAYS,那两者就没有区别了
FloatLayoutHelper 可以设置
setDragEnable
为true来实现可以拖动的效果。
StickyLayoutHelper可以设置
StickyStart
来控制吸附在顶部或者底部,这个用来设置不同Section的Header挺方便
ColumnLayoutHelper是设置几个Item占据一整行,通过设置
setWeights
让每个Item占据相应的比例宽度。
瀑布流布局,可以设置Item间的横向
hGap
和纵向vGap
间距
1拖N的布局中每个Item占剩余空间的一半。可以设置
itemCount
来控制显示几个Item。
<?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/frameLayout" android:layout_width="match_parent" android:layout_height="match_parent" tools:context=".Fragment.DiscoveryMainFragment"> <androidx.recyclerview.widget.RecyclerView android:id="@+id/main_recyclerview" android:layout_width="0dp" android:layout_height="0dp" android:paddingBottom="20dp" app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintEnd_toEndOf="parent" app:layout_constraintStart_toStartOf="parent" app:layout_constraintTop_toTopOf="parent" /> </androidx.constraintlayout.widget.ConstraintLayout>
// 1 RecyclerView.RecycledViewPool().also { it.setMaxRecycledViews(1, 6) it.setMaxRecycledViews(2, 6) it.setMaxRecycledViews(3, 6) it.setMaxRecycledViews(4, 6) it.setMaxRecycledViews(5, 6) it.setMaxRecycledViews(6, 6) it.setMaxRecycledViews(7, 6) it.setMaxRecycledViews(8, 6) it.setMaxRecycledViews(9, 6) main_recyclerview.setRecycledViewPool(it) } // 2 val layoutManager = VirtualLayoutManager(requireActivity()).also { main_recyclerview.layoutManager = it } // 3 main_recyclerview.adapter = DelegateAdapter(layoutManager, false)
这里比较简单不做过多介绍。
由于子Adapter在项目中会非常的多,所以可以把一些公共的功能抽提出来,进行代码复用
open class BaseDelegateAdapter(protected val context: Context, private val layoutHelper: LayoutHelper, private val layoutId: Int, private val count: Int, protected val mViewType: Int) : DelegateAdapter.Adapter<BaseViewHolder>() { /* 创建ViewHolder */ override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): BaseViewHolder { val v = LayoutInflater.from(parent.context).inflate(layoutId, parent, false) return BaseViewHolder(v) } /* 绑定ViewHolder */ override fun onBindViewHolder(holder: BaseViewHolder, position: Int) { } /* 多少个Item */ override fun getItemCount(): Int { return count } /* LayoutHelper */ override fun onCreateLayoutHelper(): LayoutHelper { return layoutHelper } }
BaseDelegateAdapter被抽提出来成为所有子Adapter的父类。构造函数中的context
和layoutHelper
好理解,layoutId
是对应界面的布局文件ID,count
对应的是layoutHelper
显示几个Item,mViewType
标记视图类型,供RecyclerView进行View的复用。
上面的文件中还有一个BaseViewHolder类,它是RecyclerView.ViewHolder的子类,抽取了一些方法供子类复用。可以参考 BaseRecyclerViewAdapterHelper
我们接下来实现轮播图的功能,效果如下:
// banner
implementation 'com.youth.banner:banner:2.1.0'
<com.youth.banner.Banner
android:id="@+id/main_banner"
android:layout_width="match_parent"
android:layout_height="166dp"
app:banner_auto_loop="true"
app:banner_indicator_gravity="center"
app:banner_indicator_marginBottom="21dp"
app:banner_indicator_normal_color="#80FFFFFF"
app:banner_indicator_normal_width="7dp"
app:banner_indicator_selected_color="@color/colorAccent"
app:banner_indicator_selected_width="7dp"
app:banner_indicator_space="5dp"
app:banner_infinite_loop="true" />
banner_auto_loop
- 自动开始滚动;
banner_indicator_gravity
- 指示器的位置;
banner_indicator_marginBottom
- 指示器底部间距;
banner_indicator_normal_color
- 指示器的颜色;
banner_indicator_selected_color
- 指示器选中后的颜色;
banner_indicator_space
- 指示器之间的间距;
banner_infinite_loop
- 循环滚动;
如果每个Item只是显示一张图片,可以不用自定义Adapter,Banner库有提供一些默认的Adapter。
我们每个Item显示一个图片,右下角还有个文本标签,我们自定义BannerImageTitleAdapter,代码如下:
class BannerImageTitleAdapter(data: List<HomeBanner>) : BannerAdapter<HomeBanner, BaseViewHolder>(data) { override fun onCreateHolder(parent: ViewGroup?, viewType: Int): BaseViewHolder { // viewHolder创建 val view = LayoutInflater.from(parent!!.context).inflate(R.layout.layout_item_home_banner, parent, false) view.clipViewCornerByDp(6.0F) return BaseViewHolder(view) } override fun onBindView(holder: BaseViewHolder?, data: HomeBanner?, position: Int, size: Int) { val imageView = holder?.getView<ImageView>(R.id.banner_iv) val textView = holder?.getView<TextView>(R.id.bannber_title) // 设置图片 imageView?.let { data?.let { bannerData -> Glide.with(holder!!.itemView) .load(bannerData.pic) .into(it) } } // 设置文本 textView?.let { data?.let { bannerData -> it.text = bannerData.typeTitle } } // 设置背景颜色 val shapeDrawable = holder?.getView<TextView>(R.id.bannber_title)?.background as? GradientDrawable shapeDrawable?.let { data?.let { bannerData -> it.setColor(Color.parseColor(bannerData.titleColor)) holder.getView<TextView>(R.id.bannber_title).background = it } } } }
BannerImageTitleAdapter中的这些方法是不是都很熟悉。没错Banner基于RecyclerView,所以BannerAdapter是RecyclerView.Adapter的子类,将HomeBanner和layout_item_home_banner绑定在一起,然后将这些信息提供给Banner。
data class HomeBanner(
val pic: String,
val typeTitle: String,
val titleColor: String,
val targetType: Long
)
<!-- layout_item_home_banner.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/linearLayout" android:layout_width="match_parent" android:layout_height="match_parent" android:layout_marginStart="16dp" android:layout_marginTop="16dp" android:layout_marginEnd="16dp" android:layout_marginBottom="16dp"> <ImageView android:id="@+id/banner_iv" android:layout_width="0dp" android:layout_height="0dp" android:scaleType="centerCrop" app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintEnd_toEndOf="parent" app:layout_constraintStart_toStartOf="parent" app:layout_constraintTop_toTopOf="parent" tools:srcCompat="@tools:sample/avatars" /> <TextView android:id="@+id/bannber_title" android:layout_width="wrap_content" android:layout_height="wrap_content" android:background="@drawable/left_top_corner_5_shape" android:paddingStart="5dp" android:paddingTop="5dp" android:paddingEnd="5dp" android:paddingBottom="5dp" android:text="TextView" android:textColor="#FFFFFF" android:textSize="11sp" app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintEnd_toEndOf="parent" /> </androidx.constraintlayout.widget.ConstraintLayout>
我们用HomeBannerAdapter来负责轮播图那部分的展示。
layoutHelper
我们可以使用LinearLayoutHelper,layoutId
使用的布局文件如下,count
为1,viewType
可以定义为1.
<!-- vlayout_banner.xml --> <?xml version="1.0" encoding="utf-8"?> <LinearLayout 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" android:orientation="vertical"> <com.youth.banner.Banner android:id="@+id/main_banner" android:layout_width="match_parent" android:layout_height="166dp" app:banner_auto_loop="true" app:banner_indicator_gravity="center" app:banner_indicator_marginBottom="21dp" app:banner_indicator_normal_color="#80FFFFFF" app:banner_indicator_normal_width="7dp" app:banner_indicator_selected_color="@color/colorAccent" app:banner_indicator_selected_width="7dp" app:banner_indicator_space="5dp" app:banner_infinite_loop="true" /> </LinearLayout>
HomeBannerAdapter文件中代码如下:
class HomeBannerAdapter( context: Context, layoutHelper: LayoutHelper, layoutId: Int, count: Int, mViewType: Int, private val bannerList: List<HomeBanner>, private val lifecycleOwner: LifecycleOwner ) : BaseDelegateAdapter(context, layoutHelper, layoutId, count, mViewType) { override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): BaseViewHolder { val holder = super.onCreateViewHolder(parent, viewType) holder.getView<Banner<HomeBanner, BannerImageTitleAdapter>>(R.id.main_banner).apply { // adapter = BannerImageTitleAdapter(bannerList) addBannerLifecycleObserver(lifecycleOwner) indicator = CircleIndicator(context) } return holder } }
此外,HomeBannerAdapter还多了两个构造参数,bannerList
是Banner的数据数组,lifecycleOwner
是Banner在适当的时候取消加载图片和滚动的生命周期观察者。
// 添加HomeBannerAdapter
val bannerAdapter = HomeBannerAdapter(requireActivity(), LinearLayoutHelper(), R.layout.vlayout_banner, 1, ViewType.HOME_VIEW_TYPE_BANNER, data, this)
adapters.add(bannerAdapter)
至此,轮播图功能完成了。代码有点多且零散,还是来一张图片来总结下吧。
我们先来看下下面横向滑动的需求:
通过Banner的练习,我们可以联想到可以用一个横向滑动的RecyclerView搭配LinearLayoutHelper实现。我们开始吧。
<!-- vlayout_recyclerview.xml --> <?xml version="1.0" encoding="utf-8"?> <FrameLayout 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/frame" android:layout_width="match_parent" android:layout_height="match_parent"> <androidx.recyclerview.widget.RecyclerView android:id="@+id/recyclerview_hor" android:layout_width="match_parent" android:layout_height="match_parent" android:overScrollMode="never" android:scrollbars="none" app:fastScrollEnabled="false" /> </FrameLayout>
<!-- layout_item_home_playlist --> <?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/constraint" android:layout_width="wrap_content" android:layout_height="wrap_content"> <ImageView android:id="@+id/live_iv" android:layout_width="105dp" android:layout_height="105dp" app:layout_constraintEnd_toEndOf="parent" app:layout_constraintStart_toStartOf="parent" app:layout_constraintTop_toTopOf="parent" app:srcCompat="@mipmap/default_pic" /> <LinearLayout android:id="@+id/live_tip_ll" android:layout_width="0dp" android:layout_height="wrap_content" app:layout_constraintEnd_toEndOf="parent" app:layout_constraintStart_toStartOf="parent" app:layout_constraintTop_toTopOf="parent"> <TextView android:id="@+id/live_tip_tv" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_weight="0" android:background="@drawable/right_bottom_corner_5_90percent_shape" android:ellipsize="end" android:lines="1" android:paddingStart="6dp" android:paddingTop="3dp" android:paddingEnd="6dp" android:paddingBottom="3dp" android:text="TextView" android:textColor="@color/colorPrimary" android:textSize="10sp" /> </LinearLayout> <TextView android:id="@+id/live_tv" android:layout_width="0dp" android:layout_height="wrap_content" android:layout_marginTop="4dp" android:ellipsize="end" android:lines="2" android:text="TextView" android:textColor="@color/black_21_color" android:textSize="14sp" app:layout_constraintEnd_toEndOf="parent" app:layout_constraintStart_toStartOf="parent" app:layout_constraintTop_toBottomOf="@+id/live_iv" /> </androidx.constraintlayout.widget.ConstraintLayout>
Adapter的代码如下:
class HomePlayListAdapter( context: Context, layoutHelper: LayoutHelper, layoutId: Int, count: Int, mViewType: Int, private val creatives: List<Creatives> ): BaseDelegateAdapter(context, layoutHelper, layoutId, count, mViewType) { override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): BaseViewHolder { val viewHolder = super.onCreateViewHolder(parent, viewType) // 1. viewHolder.getView<RecyclerView>(R.id.recyclerview_hor).apply { // 2 layoutManager = LinearLayoutManager(context, RecyclerView.HORIZONTAL, false) // 3 addItemDecoration(object : RecyclerView.ItemDecoration() { override fun getItemOffsets( outRect: Rect, view: View, parent: RecyclerView, state: RecyclerView.State ) { val position = parent.getChildAdapterPosition(view) if (position == RecyclerView.NO_POSITION) return when (position) { 0 -> outRect.set(context.dp2px(16.0F), 0, context.dp2px(10.0F), 0) creatives.size - 1 -> outRect.set(0, 0, context.dp2px(16.0F), 0) else -> outRect.set(0, 0, context.dp2px(10.0F), 0) } } }) // 4 adapter = object : RecyclerView.Adapter<BaseViewHolder>() { override fun onCreateViewHolder( parent: ViewGroup, viewType: Int ): BaseViewHolder { return BaseViewHolder( LayoutInflater.from(parent.context) .inflate(R.layout.layout_item_home_playlist, parent, false) ) } override fun onBindViewHolder(holder: BaseViewHolder, position: Int) { holder.getView<ImageView>(R.id.vlog_iv).apply { loadRoundCornerImage( context, EmptyEx.checkStringNull(creatives[position]?.uiElement?.image?.imageUrl) ) } holder.getView<TextView>(R.id.vlog_title_tv).text = EmptyEx.checkStringNull(creatives[position]?.uiElement?.mainTitle?.title) holder.getView<TextView>(R.id.vlog_zan_tv).text = EmptyEx.checkLongNull(creatives[position]?.resources?.get(0)?.resourceExtInfo?.playCount) .playCountString(context) } override fun getItemCount(): Int { return creatives.size } } } return viewHolder } }
代码解释如下:
onCreateViewHolder
创建ViewHolder的时候找到RecyclerView.RecyclerView.HORIZONTAL
val playAdapter = HomePlayListAdapter(requireActivity(), LinearLayoutHelper(), R.layout.vlayout_recyclerview, 1, ViewType.HOME_VIEW_TYPE_SLIDE_PLAYLIST, creative)
adapters.add(playAdapter)
至此vlayout嵌套横向RecyclerView的功能就完成了。
首页的接口有一个特殊的地方,ExtInfo在博客的列表中是Map,在直播的列表中是List。如果直接解析肯定是有问题,会直接崩溃。
解决方案是自定义Moshi的JsonAdapter。
data class ExtInfo constructor (
val liveExt: List<LiveExt>?,
val blogExt: BlogExt?
) {
constructor(liveExt: List<LiveExt>) : this(liveExt, null)
constructor(blogExt: BlogExt): this(null, blogExt)
}
如果表示直播就是给liveExt
赋值,如果表示博客就是给blogExt
赋值。
class ExtInfoAdapter { // 1 @FromJson fun fromJson(reader: JsonReader): ExtInfo { val jsonValue = reader.readJsonValue() return when (jsonValue) { is List<*> -> { var lists = mutableListOf<LiveExt>() jsonValue.forEach { val map = it as? Map<String, Any> map?.let { map -> val popularity = (map["popularity"] as Double).toLong() val verticalCover = map["verticalCover"] as String val startStreamTagName = map["startStreamTagName"] as String val title = map["title"] as String val ext = LiveExt( popularity, verticalCover, startStreamTagName, title ) lists.add(ext) } } // 2 ExtInfo(lists) } is Map<*, *> -> { var title: String? = jsonValue["moduleName"] as String? val squareFeedViewDTOList = jsonValue["squareFeedViewDTOList"] as? List<Map<String, *>> var lists = mutableListOf<BlogDetail>() squareFeedViewDTOList?.let { feedList -> for (map in feedList) { val resources = map["resource"] as? Map<String, *> val mlogBaseData = resources?.get("mlogBaseData") as? Map<String, *> val coverUrl = mlogBaseData?.get("coverUrl") as? String val id = mlogBaseData?.get("id") as? String val talk = mlogBaseData?.get("talk") as? Map<String, *> val talkDesc = talk?.get("talkDesc") as? String val mlogExt = resources?.get("mlogExt") as? Map<String, *> val likedCount = (mlogExt?.get("likedCount") as Double).toLong() lists.add(BlogDetail(id, talkDesc, coverUrl, likedCount)) } } // 3 ExtInfo(BlogExt(lists, title)) } // 4 else -> throw JsonDataException("Expected a field of type List or Map") } } }
@FromJson
表示从JSON转ExtInfo对象时候调用这个方法List<*>
的时候解析数据,调用ExtInfo(lists)
构造函数Map<*>
的时候解析数据,调用ExtInfo(BlogExt(lists, title))
构造函数// 1
val moshi = Moshi.Builder()
.add(ExtInfoAdapter())
.build()
val retrofit = Retrofit.Builder()
.baseUrl(MusicApiConstant.BASE_URL)
.client(okHttpClient)
// 2
.addConverterFactory(MoshiConverterFactory.create(moshi))
.build()
至此,Retrofit解析ExtInfo时就能自动解析不同的数据类型了。
首页的其他内容也类似,只是layout不一样,然后定义相应类型的Adapter 然后加入到添加到DelegateAdapter中。这样其他的工作就交给vlayout去自动实现了。
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。