Android Banner - ViewPager 01
Android Banner - ViewPager 01
日常开发过程中会使用banner展示图片信息,起到推广的作用。
常见的banner实现方式有以下几种
-
ViewPager
-
ViewPager2
-
MotionLayout Carousel
今天我们使用ViewPager来实现一个Banner。
banner实现以下功能
-
无限轮播
-
adpter抽取
-
切换动效实现
首先自定义View,VPBanner
class VPBanner : ViewPager {
constructor(context: Context) : this(context, null)
constructor(context: Context, attrs: AttributeSet?) : super(context, attrs){
// 读取自定义的属性
}
}
自定义adapter
abstract class VPAdapter<T> : PagerAdapter() {
private val mData = mutableListOf<T>()
fun setData(data: List<T>) {
mData.clear()
if (data.size > 1) {
// 数组组织一下,用来实现无限轮播
mData.add(data[data.size - 1])
mData.addAll(data)
mData.add(data[0])
} else {
mData.addAll(data)
}
}
abstract fun bindView(container: ViewGroup, position: Int, data: T): View
override fun getCount() = mData.size
override fun isViewFromObject(view: View, obj: Any) = view == obj
override fun getItemPosition(obj: Any): Int {
return POSITION_NONE
}
override fun instantiateItem(container: ViewGroup, position: Int): Any {
return mData.getOrNull(position)?.let { data ->
bindView(container, position, data).apply {
container.addView(this@apply)
}
} ?: super.instantiateItem(container, position)
}
override fun destroyItem(container: ViewGroup, position: Int, obj: Any) {
(obj as? View)?.let {
container.removeView(it)
}
}
// 单个page 站 banner 的比重,默认为1
override fun getPageWidth(position: Int) = 1F
}
基本使用
item中使用imageview和text显示信息
// 设置adapter和数据源
mBinding.vpBanner.adapter = MyVpAdapter().apply {
setData(DataStore.getImageData())
}
无限轮播原理
1 数据组织
将原始数组的最有一个添加到数据集的开头,接着放入原始数据,最后追加原始数据中的第一个到数据集的最后。
嫁入原始数据是ABC,组织后的数据时CABCA
2 viewpager监听处理
当选中了第一个数据时,即C数据,要想实现循环,C的前后分别是B和A,和整理后的数据集中第4个的前后数据一致
当选中了最后一个数据时,即A数据,要想实现循环,A的前后分别时C和B,和整理后的数据集中第2个的前后数据一致
当选中第一个和最后一个时,我们将viewpager当前指向的item变更一下就可以实现无限循环的效果了。
class VPLoop(private val banner: VPBanner) : ViewPager.OnPageChangeListener {
private var mCurrent = 1
override fun onPageScrolled(position: Int, positionOffset: Float, positionOffsetPixels: Int) {
}
override fun onPageSelected(position: Int) {
this.mCurrent = position
}
override fun onPageScrollStateChanged(state: Int) {
// adapter 为null 或者 page count <= 1 直接返回
if (banner.adapter == null || banner.adapter!!.count <= 1) return
// 非静止状态,直接返回
if (state != ViewPager.SCROLL_STATE_IDLE) return
// 下标索引从0开始
if (mCurrent == 0) {
banner.setCurrentItem(banner.adapter!!.count - 2, false)
} else if (mCurrent == banner.adapter!!.count - 1) {
banner.setCurrentItem(1, false)
}
}
}
3 优化使用
viewpager中增加loop字段,判断是否开启无限轮播,提供重置轮播和取消轮播的方法
class VPBanner : ViewPager {
private var mLoop: VPLoop? = null
var loop = true
fun resetLoop() {
if (!loop) {
setCurrentItem(0, false)
return
}
if (mLoop == null) {
mLoop = VPLoop(this)
}
mLoop?.let {
setCurrentItem(1, false)
removeOnPageChangeListener(it)
addOnPageChangeListener(it)
}
}
fun cancelLoop() {
mLoop?.let { removeOnPageChangeListener(it) }
}
}
4 设置时长和切换动效
fun setDuration(time: Int) {
try{
val field = ViewPager::class.java.getDeclaredField("mScroller")
field?.isAccessible = true
field?.set(this,FixedScroller(context,time))
}catch (ex:Exception){
Log.e(TAG,"set duration error",ex)
}
}
自定义PageTransformer,参考:利用ViewPage的PagerTransformer定制页面切换效果_pagetransformer左侧堆叠_wandryoung的博客-CSDN博客
class RiseInTransformer:ViewPager.PageTransformer {
companion object{
private const val MIN_SCALE = 0.72F
private const val MIN_ALPHA = 0.5F
}
override fun transformPage(page: View, position: Float) {
if(position<0F){
page.translationX = 0F
}else if(position <= 1){
page.translationX = (-position * page.width)
page.scaleX = (1F -(1F - MIN_SCALE) * position)
page.scaleY = (1F -(1F - MIN_SCALE) * position)
page.alpha = (1F -(1F - MIN_ALPHA) * position)
}else{
page.translationX = (-position * page.width)
page.scaleX = MIN_SCALE
page.scaleY = MIN_SCALE
page.alpha = MIN_ALPHA
}
}
}
5 使用自定义属性来实现配置轮播以及切换时长
attrs配置
<?xml version="1.0" encoding="utf-8"?>
<resources>
<declare-styleable name="VPBanner">
<attr name="vp_duration" format="integer" />
<attr name="vp_loop" format="boolean" />
</declare-styleable>
</resources>
属性读取使用
constructor(context: Context) : this(context, null)
constructor(context: Context, attrs: AttributeSet?) : super(context, attrs) {
// 读取自定义的属性
val typedArray = context.obtainStyledAttributes(attrs, R.styleable.VPBanner)
this.loop = typedArray.getBoolean(R.styleable.VPBanner_vp_loop, false)
this.mDuration = typedArray.getInt(R.styleable.VPBanner_vp_duration, DEFAULT_DURATION)
typedArray?.recycle()
setDuration(mDuration)
}
6 小优化,看起来更好看点
加圆角,自定义圆角控件,使用它作为item的跟布局
class RoundCornerFrameLayout : FrameLayout {
constructor(
context: Context,
attrs: AttributeSet?,
style: Int
) : super(context, attrs, style) {
}
constructor(context: Context, attrs: AttributeSet?) : this(context, attrs, 0)
constructor(context: Context) : this(context, null)
private val path by lazy {
Path().apply {
addRoundRect(RectF(0F, 0F, width + 0F, height + 0F), 48F, 48F, Path.Direction.CW)
}
}
// 为了方便写死一些数据
override fun dispatchDraw(canvas: Canvas?) {
canvas?.let {
it.clipPath(path)
}
super.dispatchDraw(canvas)
}
}
设置间距
mBinding.vpBanner.pageMargin = 21
设置一页展示多个
<?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:layout_width="match_parent"
android:clipChildren="false"
android:layerType="software"
android:layout_height="match_parent"
tools:context=".MainActivity">
<com.hlox.android.vpbanner.VPBanner
android:id="@+id/vp_banner"
android:layout_margin="16dp"
android:layout_width="match_parent"
android:layout_height="180dp"
app:vp_loop="true"
app:vp_duration="1000"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>
全部源码地址: