Android 深入理解ViewPager2原理及其实践(下篇)


一、深入理解ViewPager2原理及其实践(上篇)
二、深入理解ViewPager2原理及其实践(下篇)

本篇文章主要介绍基于ViewPager2(以下简称VP2)实现的一个Banner轮播库。

一 效果图

功能示例
基本使用
仿淘宝搜索栏上下轮播

1.1 源码地址

上述示例效果及更多功能源码参见:lib_viewpager2

1.2 API介绍

  • 使用举例
val mModels = mutableListOf(MConstant.IMG_1, MConstant.IMG_2, MConstant.IMG_3)

//多个转换动画
val multiTransformer = CompositePageTransformer()
multiTransformer.addTransformer(MarginPageTransformer(20))
multiTransformer.addTransformer(ZoomOutPageTransformer())

mMVPager2.setModels(mModels) //设置轮播数据
    .setIndicatorShow(true) //设置轮播指示器
    .setOffscreenPageLimit(1) //离屏缓存数量
    .setLoader(DefaultLoader()) //设置ItemView加载器 可以自定义Item样式
    .setPagePadding(50, 0, 50, 0) //设置一屏三页
    .setPageTransformer(multiTransformer) //转换动画
    .setOrientation(MVPager2.ORIENTATION_HORIZONTAL) //轮播方向
    .setUserInputEnabled(true) //控制是否可以触摸滑动 默认为true
    .setAutoPlay(false) //设置自动轮播
    .setPageInterval(3000L) //轮播间隔
    .setAnimDuration(500) //切换动画执行时间
    .setOnBannerClickListener(object : OnBannerClickListener {
        override fun onItemClick(position: Int) {
            //Item点击
            showToast("position is $position")
        }
     })
    .registerOnPageChangeCallback(object : ViewPager2.OnPageChangeCallback() {
      //设置页面改变时的回调
    })
    .start() //开始

如果需要刷新整体数据,可以像下面进行增量更新:

//使用DiffUtil进行增量数据更新 newList:更新后的数据Models
mMVPager2.submitList(newList)

注意:使用增量更新时,如果开发语言是Java,需要针对实体类重写hashCode()equals()方法,否则增量更新可能会失效;而如果开发语言为kotlin,则实体类(data class xxx)不需要特殊处理,因为系统已经自动帮我们重写了这两个方法。

  • API介绍
API备注
setModels(list: MutableList< String>)设置轮播数据
submitList(newList: MutableList< String>)使用DiffUtil进行增量数据更新
setAutoPlay(isAutoPlay: Boolean)设置自动轮播 true-自动 false-手动
setUserInputEnabled(inputEnable: Boolean)设置MVPager2是否可以滑动 true-可以滑动 false-禁止滑动
setIndicatorShow(isIndicatorShow: Boolean)是否展示轮播指示器 true-展示 false-不展示
setPageInterval(autoInterval: Long)设置自动轮播时间间隔
setAnimDuration(animDuration: Int)设置轮播切换时的动画持续时间 通过反射改变系统自动切换的时间
注意:这里设置的animDuration值需要小于setPageInterval()中设置的autoInterval值
setOffscreenPageLimit(@OffscreenPageLimit limit: Int)设置离屏缓存数量 默认是OFFSCREEN_PAGE_LIMIT_DEFAULT = -1
setPagePadding(left: Int = 0, top: Int = 0, right: Int = 0, bottom: Int = 0)设置一屏多页
setPageTransformer(transformer: CompositePageTransformer)设置ItemView切换动画, CompositePageTransformer可以同时添加多个ViewPager2.PageTransformer
setOnBannerClickListener(listener: OnBannerClickListener)设置Banner的ItemView点击
registerOnPageChangeCallback(callback: ViewPager2.OnPageChangeCallback)设置页面改变时的回调
setOrientation(@ViewPager2.Orientation orientation: Int)设置轮播方向,横竖方向:ORIENTATION_HORIZONTAL 或 ORIENTATION_VERTICAL
setLoader(loader: ILoader< View>)设置ItemView加载器
isAutoPlay()是否自动轮播

二 核心实现思路

2.1 无限轮播

为了实现无限轮播,首先对原始数据进行扩充,如下图所示:
扩展数据
在真实数据的前后各增加2条数据,添加规则已经在图片中注明了。

private val autoRunnable: Runnable = object : Runnable {
        override fun run() {
            if (mRealCount > 1 && mIsAutoPlay) {
                mCurPos = mCurPos % mExtendModels.size + 1
                when (mCurPos) {
                    //扩展数据之后,滑动到倒数第2条数据时,改变轮播位置
                    exSecondLastPos() -> {
                        mSelectedValid = false
                        //跳转到正数第2条数据,注意这里smoothScroll设置为false,即不会有跳转动画
                        mViewPager2.setCurrentItem(1, false)
                        //立即执行,会走到下面的else中去 最终会展示正数第3条的数据,达到无限轮播的效果
                        post(this)
                    }
                    else -> {
                        mSelectedValid = true
                        mViewPager2.currentItem = mCurPos
                        //延迟执行
                        postDelayed(this, AUTO_PLAY_INTERVAL)
                    }
                }
            }
        }
    }

上面注释中已经将无限轮播的逻辑写明了。以上图扩展后的数据为例,当VP2滑动到第6条数据(position是5,value是a)时,立即跳转到第2条数据(position是1,value是c),但是此时还未来得及展示,立即会通过post(this)继续执行,从而跳转到了第3条数据(position是2,value是a),可以看到跟第6条的数据是一样的,从而达到了无限轮播的效果。当设置完上述的Runnable后,通过Handler发送Message开始执行循环:

fun startAutoPlay() {
   removeCallbacks(autoRunnable)
   postDelayed(autoRunnable, AUTO_PLAY_INTERVAL)
}

以上是自动轮播的实现场景,另外还有手动轮播,主要是在ViewPager2.OnPageChangeCallback#onPageScrollStateChanged(state: Int)回调中根据VP2.currentItem得到当前Item的位置判断下一个滑动位置的,具体跳转逻辑跟自动轮播是一样的。这里注意一点:state 必须是ViewPager2.SCROLL_STATE_DRAGGING,因为这个值可以确保只在手指触摸滑动时才会触发,自动轮播时并不会触发这里的逻辑。

2.2 轮播动画过渡

主要通过LayoutManager#smoothScrollToPosition()中通过LinearSmoothScroller#calculateTimeForScrolling()自定义速率:

/**
 * 自定义LinearLayoutManager,自定义轮播速率
 */
class LayoutManagerProxy(
    val context: Context,
    private val layoutManager: LinearLayoutManager,
    private val customSwitchAnimDuration: Int = 0,
) : LinearLayoutManager(
    context, layoutManager.orientation, false
) {

    override fun smoothScrollToPosition(
        recyclerView: RecyclerView?,
        state: RecyclerView.State?,
        position: Int
    ) {
        val linearSmoothScroller =
            LinearSmoothScrollerProxy(context, customSwitchAnimDuration)
        linearSmoothScroller.targetPosition = position
        startSmoothScroll(linearSmoothScroller)
    }

  internal class LinearSmoothScrollerProxy(
        context: Context,
        private val customSwitchAnimDuration: Int = 0
    ) : LinearSmoothScroller(context) {

        /**
         * 控制轮播切换速度
         */
        override fun calculateTimeForScrolling(dx: Int): Int {
            return if (customSwitchAnimDuration != 0)
                customSwitchAnimDuration
            else
                super.calculateTimeForScrolling(dx)
        }
    }
}

2.3 处理嵌套滑动冲突

上篇文章中已经介绍过如果处理滑动冲突,这里先将代码贴出来:

    /**
     * 处理嵌套滑动冲突
     */
    override fun onInterceptTouchEvent(ev: MotionEvent): Boolean {
        handleInterceptTouchEvent(ev)
        return super.onInterceptTouchEvent(ev)
    }

    private fun handleInterceptTouchEvent(ev: MotionEvent) {
        val orientation = mViewPager2.orientation
        if (mRealCount <= 0 || !mUserInputEnable) {
            parent.requestDisallowInterceptTouchEvent(false)
            return
        }
        when (ev.action) {
            MotionEvent.ACTION_DOWN -> {
                mInitialX = ev.x
                mInitialY = ev.y
                parent.requestDisallowInterceptTouchEvent(true)
            }
            MotionEvent.ACTION_MOVE -> {
                val dx = (ev.x - mInitialX).absoluteValue
                val dy = (ev.y - mInitialY).absoluteValue
                if (dx > mTouchSlop || dy > mTouchSlop) {
                    val disallowIntercept =
                        (orientation == ViewPager2.ORIENTATION_HORIZONTAL && dx > dy)
                                || (orientation == ViewPager2.ORIENTATION_VERTICAL && dx < dy)
                    parent.requestDisallowInterceptTouchEvent(disallowIntercept)
                }
            }
            MotionEvent.ACTION_UP, MotionEvent.ACTION_CANCEL -> {
                parent.requestDisallowInterceptTouchEvent(false)
            }
        }
    }

主要就是在onInterceptTouchEvent中通过内部拦截法requestDisallowInterceptTouchEvent()进行处理,如果嵌套滑动中的内部控件需要滑动时,就控制外部父View不拦截事件,设置为requestDisallowInterceptTouchEvent(true);反之则让外部父View拦截事件,设置为requestDisallowInterceptTouchEvent(false)

MotionEvent.ACTION_DOWN状态时一定不能让父View拦截,否则后续事件都不会传入子View中了;MotionEvent.ACTION_MOVE状态时根据VP2的方向及滑动距离判断,当是横向滑动X轴距离>Y轴距离或当是竖直滑动Y轴距离>X轴距离时,都会控制父View不拦截事件。

2.4 配合DiffUtil增量更新

class PageDiffUtil(private val oldModels: List<Any>, private val newModels: List<Any>) :
    DiffUtil.Callback() {

    /**
     * 旧数据
     */
    override fun getOldListSize(): Int = oldModels.size

    /**
     * 新数据
     */
    override fun getNewListSize(): Int = newModels.size

    /**
     * DiffUtil调用来决定两个对象是否代表相同的Item。true表示两个Item相同(表示View可以复用),false表示不相同(View不可以复用)
     * 例如,如果你的项目有唯一的id,这个方法应该检查它们的id是否相等。
     */
    override fun areItemsTheSame(oldItemPosition: Int, newItemPosition: Int): Boolean {
        return oldModels[oldItemPosition]::class.java == newModels[newItemPosition]::class.java
    }

    /**
     * 比较两个Item是否有相同的内容(用于判断Item的内容是否发生了改变),
     * 该方法只有当areItemsTheSame (int, int)返回true时才会被调用。
     */
    override fun areContentsTheSame(oldItemPosition: Int, newItemPosition: Int): Boolean {
        return oldModels[oldItemPosition] == newModels[newItemPosition]
    }

    /**
     * 该方法执行时机:areItemsTheSame(int, int)返回true 并且 areContentsTheSame(int, int)返回false
     * 该方法返回Item中的变化数据,用于只更新Item中变化数据对应的UI
     */
    override fun getChangePayload(oldItemPosition: Int, newItemPosition: Int): Any? {
        return super.getChangePayload(oldItemPosition, newItemPosition)
    }
}

调用方:

    /**
     * use[DiffUtil] 增量更新数据
     * @param newList 新数据
     */
    fun submitList(newList: MutableList<String>) {
        //传入新旧数据进行比对
        val diffUtil = PageDiffUtil(mModels, newList)
        //经过比对得到差异结果
        val diffResult = DiffUtil.calculateDiff(diffUtil)
        //NOTE:注意这里要重新设置Adapter中的数据
        setModels(newList)
        //将数据传给adapter,最终通过adapter.notifyItemXXX更新数据
        diffResult.dispatchUpdatesTo(this)
    }

2.5 自定义Item样式

首先定义一个接口,接口中的两个方法分别用来创建ItemView及对ItemView进行赋值:

interface ILoader<T : View> {
    fun createView(context: Context): T
    fun display(context: Context, content: Any, targetView: T)
}

ItemView基类,默认创建的是ImageView

abstract class BaseLoader : ILoader<View> {

    override fun createView(context: Context): View {
        val imageView = ImageView(context)
        imageView.scaleType = ImageView.ScaleType.CENTER_CROP
        imageView.layoutParams = ViewGroup.LayoutParams(
            ViewGroup.LayoutParams.MATCH_PARENT,
            ViewGroup.LayoutParams.MATCH_PARENT
        )
        return imageView
    }
}

默认DefaultLoader继承自BaseLoader并在display()中通过Glide加载ImageView

/**
 * 默认为ImageView加载
 */
class DefaultLoader : BaseLoader() {

    override fun createView(context: Context): View {
        return super.createView(context)
    }

    override fun display(context: Context, content: Any, targetView: View) {
        Glide.with(context).load(content).into(targetView as ImageView)
    }
}

当然,如果不想加载ImageView,可以在子类中进行重写,比如我们想创建的ItemView是一个TextView,可以像下面这么写:

/**
 * TextView视图
 */
class TextLoader : BaseLoader() {

    @ColorRes
    private var mBgColor: Int = R.color.white

    @ColorRes
    private var mTextColor: Int = R.color.black
    private var mTextGravity: Int = Gravity.CENTER
    private var mTextSize: Float = 14f

    override fun createView(context: Context): View {
        val frameLayout = FrameLayout(context).apply {
            layoutParams = ViewGroup.LayoutParams(
                ViewGroup.LayoutParams.MATCH_PARENT,
                ViewGroup.LayoutParams.MATCH_PARENT
            )
            setBackgroundColor(context.resources.getColor(mBgColor))
        }
        val textView = TextView(context).apply {
            gravity = mTextGravity
            setTextColor(context.resources.getColor(mTextColor))
            textSize = mTextSize
            layoutParams = FrameLayout.LayoutParams(
                ViewGroup.LayoutParams.MATCH_PARENT,
                ViewGroup.LayoutParams.MATCH_PARENT
            )
        }
        frameLayout.addView(textView)
        return frameLayout
    }

    override fun display(context: Context, content: Any, targetView: View) {
        val frameLayout = targetView as FrameLayout
        val childView = frameLayout.getChildAt(0)
        if (childView is TextView) {
            childView.text = content.toString()
        }
    }

    fun setBgColor(@ColorRes bgColor: Int): TextLoader {
        this.mBgColor = bgColor
        return this
    }

    fun setTextColor(@ColorRes textColor: Int): TextLoader {
        this.mTextColor = textColor
        return this
    }

    fun setGravity(gravity: Int): TextLoader {
        this.mTextGravity = gravity
        return this
    }

    fun setTextSize(textSize: Float): TextLoader {
        this.mTextSize = textSize
        return this
    }
}

最终是在RecyclerView.Adapter中如下调用:

class MVP2Adapter : RecyclerView.Adapter<MVP2Adapter.PageViewHolder>() {
    override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): PageViewHolder {
        //创建要显示的ItemView
        var itemShowView = mLoader?.createView(parent.context)
        return PageViewHolder(itemShowView)
        }

    override fun onBindViewHolder(holder: PageViewHolder, position: Int) {
          val contentStr = mModels[position]
          //ItemView展示数据
          mLoader?.display(holder.itemShowView.context, contentStr, holder.itemShowView)
    }
}

通过接口的方式将具体实现进行隔离,对扩展开放,对修改关闭,达到了开闭效果。调用方如果想自定义Item样式,可以自行实现ILoader并实现自己想要的样式即可。

  • 4
    点赞
  • 7
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
### 回答1: 要在 Android 中使用 ExoPlayer2ViewPager2 播放视频,需要以下步骤: 1. 安装 ExoPlayer2 库:在 build.gradle 文件中添加以下依赖项: ``` implementation 'com.google.android.exoplayer:exoplayer:2.X.X' ``` 2. 创建一个 ExoPlayer2 实例:在您的 Activity 或 Fragment 中创建一个 ExoPlayer 实例,并设置要播放的媒体文件。 3. 创建一个 ExoPlayerView:在您的布局文件中创建一个 ExoPlayerView 并将其与 ExoPlayer 实例关联。 4. 创建一个 ViewPager2:在您的布局文件中创建一个 ViewPager2,并将其填充为包含 ExoPlayerView 的 Fragment。 5. 设置 ViewPager2 适配器:创建一个适配器并将其与 ViewPager2 关联,以显示每个 Fragment。 这些步骤将为您提供一个可在 ViewPager2 中播放视频的基础架构。根据您的需求,您可以对代码进行进一步的调整。 ### 回答2: Android ExoPlayer2 是一款强大的视频播放器库,而 ViewPager2 则是一个用于实现滑动页面的视图容器。结合这两个库,可以实现在 ViewPager2 内播放视频的功能。 首先,我们需要在布局文件中定义一个 ViewPager2 控件,并在适当的时候,将视频的 URL 或本地路径传递给适配器。接着,在适配器中,我们可以使用 ExoPlayer2 创建一个播放器对象,并将视频源设置为这个播放器。 在 ViewPager2 的适配器中,我们需要实现以下方法: 1. `onCreateViewHolder`:用于创建 RecyclerView.ViewHolder,即每个页面的视图。 2. `onBindViewHolder`:用于将数据与视图绑定在一起,例如设置视频的 URL 或本地路径。 3. `onViewAttachedToWindow`:在视图被添加到窗口时被调用,我们可以在这个方法内初始化和启动播放器。 4. `onViewDetachedFromWindow`:在视图从窗口中移除时被调用,我们可以在这个方法内释放播放器资源。 在 `onViewAttachedToWindow` 方法中,我们可以执行以下步骤来初始化和启动播放器: 1. 创建一个 SimpleExoPlayer 实例。 2. 创建一个 DefaultTrackSelector,并将其应用在播放器上。 3. 为播放器设置 MediaSource,即视频资源。可以使用三种 MediaSource:HLS、DASH 和普通的 progressive。 4. 将 SurfaceView 添加到视图中,并通过 `exoPlayer.setVideoSurfaceView(surfaceView);` 将其附加到播放器上。 5. 最后,调用 `exoPlayer.prepare()` 和 `exoPlayer.setPlayWhenReady(true)`,分别用于准备播放和自动播放视频。 在 `onViewDetachedFromWindow` 方法中,我们可以执行以下步骤来释放播放器资源: 1. 调用 `exoPlayer.setPlayWhenReady(false)`,暂停播放器。 2. 调用 `exoPlayer.release()`,释放播放器资源。 通过这种方式,我们可以在 ViewPager2 中实现视频的滑动播放功能。每次滑动到一个新的页面时,我们可以在 `onViewAttachedToWindow` 方法中初始化和启动播放器,在离开页面时,在 `onViewDetachedFromWindow` 方法中释放播放器资源。这样,我们可以平滑地在 ViewPager2 中切换播放的视频内容。 ### 回答3: Android ExoPlayer2是一个功能强大的用于播放视频和音频的开源库。ViewPager2是一个支持左右滑动切换页面的控件。将这两个库结合起来,可以实现在ViewPager2中播放视频的功能。 首先,需要在项目的build.gradle文件中添加ExoPlayer2ViewPager2的依赖库。通过在dependencies中添加以下代码,引入所需的库: ```groovy implementation 'com.google.android.exoplayer:exoplayer:2.x.x' implementation 'androidx.viewpager2:viewpager2:1.0.0' ``` 然后,在ViewPager2的Adapter中创建一个自定义的Fragment,并在该Fragment中添加ExoPlayer2的播放器用来播放视频。在该Fragment的布局文件中,可以添加一个用于显示视频画面的SurfaceView或TextureView。 在Fragment中,需要初始化ExoPlayer2的播放器实例,并设置要播放的视频资源。可以通过以下代码实现: ```java // 在Fragment中初始化ExoPlayer2的播放器实例 SimpleExoPlayer exoPlayer = new SimpleExoPlayer.Builder(context).build(); // 设置要播放的视频资源 Uri videoUri = Uri.parse("视频资源的URL或本地路径"); MediaSource mediaSource = new ProgressiveMediaSource.Factory( new DefaultDataSourceFactory(context, "Your Application Name")) .createMediaSource(videoUri); exoPlayer.prepare(mediaSource); ``` 在ViewPager2的Adapter中,创建一个List来保存所有的Fragment,以便在ViewPager2中展示。在Activity中,创建一个ViewPager2的实例,并设置Adapter和页面切换监听器,如下所示: ```java ViewPager2 viewPager2 = findViewById(R.id.viewPager2); ViewPager2Adapter adapter = new ViewPager2Adapter(fragmentList); viewPager2.setAdapter(adapter); // 添加页面切换监听器 viewPager2.registerOnPageChangeCallback(new ViewPager2.OnPageChangeCallback() { @Override public void onPageSelected(int position) { super.onPageSelected(position); // 当页面切换时,暂停之前页面的视频播放 exoPlayer.setPlayWhenReady(false); } }); ``` 通过以上步骤,就可以在ViewPager2中播放视频了。需要注意的是,在页面切换时,可以通过设置exoPlayer.setPlayWhenReady(false)来暂停之前页面的视频播放,保证用户在切换页面时不会同时播放多个视频。 希望可以帮到你!

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

_小马快跑_

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值