Android Banner - ViewPager 01

日常开发过程中会使用banner展示图片信息,起到推广的作用。

常见的banner实现方式有以下几种

  1. ViewPager

  2. ViewPager2

  3. MotionLayout Carousel

今天我们使用ViewPager来实现一个Banner。

banner实现以下功能

  1. 无限轮播

  2. adpter抽取

  3. 切换动效实现

首先自定义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>

全部源码地址: