## 版本升级日志 **Version 1.0.0-rc3** (2022.10.01)
1、MVPager2 Banner支持非轮播模式 ## 一 效果图 |功能 | 示例 | |--|--| | **基本使用** | | | **仿淘宝搜索栏上下轮播** | | | **仿淘宝、京东Banner滑动查看图文详情** | | ### 使用方式 ``` 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)不需要特殊处理,因为系统已经自动帮我们重写了这两个方法。 ### 1.1 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, private val newModels: List) : 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) { //传入新旧数据进行比对 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 { fun createView(context: Context): T fun display(context: Context, content: Any, targetView: T) } ``` `ItemView`基类,默认创建的是`ImageView`: ``` abstract class BaseLoader : ILoader { 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() { 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`并实现自己想要的样式即可。