banner 轮播图控件
自定义组合控件实现轮播图,内部可以用recycleview或者viewpage实现,其主要需要完成的有三个点:
- 无限循环方式的实现
- 指示器的实现
- item view的transform操作
第一个实现无限循环的方式:
内部用viewpager实现轮播的话,可以在数据集合的大小方面做处理。一般做法是在第一个数据之前加上最后一条数据,最后一条数据之后再加上第一条数据,即数据集合实际大小为size + 2。然后再监听到position为0 或者 是最后一条数据时,移动currentItem至实际位置。这样做存在一个问题,再重新移动currentItem时,对应的移动监听会回调两次。
网上大部分的写法是,集合大小给一个较大的值,例如几百,然后再处理position,这样不会需要经常触发setCurrentItem。
adapter 中 重写getItemCount() 方法
override fun getItemCount(): Int {
return if (isCanLoop) BannerUtils.MAX_VALUE_SIZE else dataList.size
}
在select监听中处理:
override fun onPageSelected(position: Int) {
super.onPageSelected(position)
val realPosition = BannerUtils.getRealPosition(position,mBannerSize)
if (position == 0 || position == BannerUtils.MAX_VALUE_SIZE - 1) {
resetCurrentItem(realPosition) // 重新移动至中间位置
}
if (inDecorateViews.isNotEmpty()) { // 设置指示器 选中状态
inDecorateViews[currentPosition].isSelected = false
inDecorateViews[realPosition].isSelected = true
}
currentPosition = realPosition
pageChangeCallback?.onPageSelected(realPosition)
}
private fun resetCurrentItem(item: Int) {
mViewPager.setCurrentItem(getResetItem(item,adapter.dataList.size),false)
}
private fun getResetItem(item: Int, pageSize: Int) : Int { // 轮播从中间部分开始
return if (!isCanLoop || pageSize <= 0) item
else BannerUtils.MAX_VALUE_SIZE / 2 - BannerUtils.MAX_VALUE_SIZE / 2 % pageSize + item
}
内部用recycleview实现,可以重写layoutManager以实现无限循环.
layoutManager 主要工作是
1.测量布局子view
测量布局itemView 主要在方法onLayoutChilder 里操作。
通过 addView方法添加view
measureChildWithMargins 方法测量itemView
layoutDecorated 方法布局itemView。
2.滚动事件的处理
在 canScrollHorizontally 方法判断是否可以横向滚动。
在 scrollHorizontallyBy 方法里处理滚动事件。第一个参数为正为向右滚动,
为负为向左滚动。
正常操作滑动至边界后,返回0不在移动。
可以在这里处理当滑动至边界后,重新取第一个view,继续布局。
final View scrap = recycler.getViewForPosition(adapterPosition);
measureChildWithMargins(scrap, 0, 0);
resetViewProperty(scrap);
// we need i to calculate the real offset of current view
final float targetOffset = getProperty(i) - mOffset;
layoutScrap(scrap, targetOffset);
final float orderWeight = mEnableBringCenterToFront ?
setViewElevation(scrap, targetOffset) : adapterPosition;
if (orderWeight > lastOrderWeight) {
addView(scrap);
} else {
addView(scrap, 0);
}
if (i == currentPos) currentFocusView = scrap;
lastOrderWeight = orderWeight;
positionCache.put(i, scrap); // 缓存view
3.缓存并重用子view
layoutManager中 用recycle 来缓存itemView。
第二个指示器的实现:
一般是自定义的banner内加一个LinearLayout,再在里面添加View。
因为需要viewpager 的adapter 中数据集合的大小确定实际view的个数,所以需要在设置了adapter数据之后,再设置指示器。
private fun initInDecoration(drawables: Array<Drawable>) { // 指示器
val inLinear = LinearLayout(context)
inLinear.orientation = inDecorateOrientation
val params = LayoutParams(-2,-2).apply {
this.gravity = inDecorateGravity
bottomMargin = inDecorateBtMargin
topMargin = inDecorateTopMargin
leftMargin = inDecorateLgMargin
rightMargin = inDecorateRgMargin
} // 自适应布局
addInDecorateIv(inLinear,drawables)
if (inDecorateViews.size > currentPosition) inDecorateViews[currentPosition].isSelected = true
addView(inLinear,params)
}
private fun addInDecorateIv(ll: LinearLayout,drawable: Array<Drawable>) {
drawable.forEach {
val iv = ImageView(context)
iv.background = it
val params = LinearLayout.LayoutParams(-2,-2).apply {
rightMargin = decorateRgMargin
}
inDecorateViews.add(iv)
ll.addView(iv,params)
}
}
设置指示器view的background,默认用一个圆点显示,创建drawable的代码:
fun createDrawable(color: Int,pointSize: Int) : Drawable{
val drawable = GradientDrawable()
drawable.setColor(color)
drawable.setBounds(0,0, pointSize, pointSize)
drawable.shape = GradientDrawable.OVAL
drawable.cornerRadius = (pointSize / 2).toFloat()
drawable.setSize(pointSize,pointSize)
return drawable
}
第三个transform的实现:
这个其实只需要回调viewpager2的设置transform的方法就行了。
设置transform 需要重写transform方法,
第一个参数:滑动的itemView。
第二个参数:position:view移动偏移量。
范围:从-∞ 到 +∞。
主要根据position,对itemView进行一些缩放平移操作。
when{
position <= -1 -> { // page 已滑至最左边
page.apply {
// scaleX = MIN_SCALE
scaleY = MIN_SCALE
// rotationY = rotate
}
}
position < 0 -> { // page 逐渐向左滑动 position : -1 -> 0 scale : 0.8 -> 1 position 为0 则滑至中间位置
page.apply {
// scaleX = scale
scaleY = MIN_SCALE + (1 + position) * (1 - MIN_SCALE)
// rotationY = rotate
}
}
position < 1 -> { // page 逐渐向右滑动 0 -> 1 1 -> 0.8
page.apply {
// scaleX = scale
scaleY = MIN_SCALE + (1 - position) * (1 - MIN_SCALE)
// rotationY = -rotate
}
}
else -> { // page 位于最右边
page.apply {
// scaleX = scale
scaleY = MIN_SCALE
// rotationY = -rotate
}
}
在viewpager 中有一个设置pageMargin的方法。在viewpager2中没有,
想要设置它的话,可以通过在transform中设置x轴平移来完成。
val offset = position * pageMargin
// 如果布局方向为RTL 则为 -offset
// viewpager2 中没有设置pagerMargin 得方法 用这个方法设置view 得偏移量,作为page 得间距
page.translationX = offset