This article mainly introduces a Banner rotation library based on ViewPager2(VP2 for short).

A rendering

function The sample
The basic use
Imitation Taobao search bar up and down round broadcast

1.1 Source Code address

See lib_viewpager2 for more

1.2 API is introduced

API note
setModels(list: MutableList< String>) Set the multicast data
submitList(newList: MutableList< String>) Incremental data updates are performed using DiffUtil
setAutoPlay(isAutoPlay: Boolean) Set automatic multicast to true- Automatic false- Manual
setUserInputEnabled(inputEnable: Boolean) Sets whether MVPager2 can slide true- can slide false- disable slide
setIndicatorShow(isIndicatorShow: Boolean) Whether to display the multicast indicator true- Display false- do not display
setPageInterval(autoInterval: Long) Set the automatic multicast interval
setAnimDuration(animDuration: Int) Set the duration of animation when the rotation switch through reflection to change the system automatic switch time

Pay attention to: The animDuration value must be less than the autoInterval value set in setPageInterval()
setOffscreenPageLimit(@OffscreenPageLimit limit: Int) The default value is OFFSCREEN_PAGE_LIMIT_DEFAULT = -1
setPagePadding(left: Int = 0, top: Int = 0, right: Int = 0, bottom: Int = 0) Set one screen to multiple pages
setPageTransformer(transformer: CompositePageTransformer) Set switch ItemView animation, CompositePageTransformer can also add multiple ViewPager2. PageTransformer
setOnBannerClickListener(listener: OnBannerClickListener) To set the Banner ItemView click
registerOnPageChangeCallback(callback: ViewPager2.OnPageChangeCallback) Set the callback when the page changes
setOrientation(@ViewPager2.Orientation orientation: Int) Set the rotation direction: ORIENTATION_HORIZONTAL or ORIENTATION_VERTICAL
setLoader(loader: ILoader< View>) Set up the ItemView loader
isAutoPlay() Whether automatic rotation

Two core implementation ideas

2.1 Infinite round broadcast

In order to realize infinite rotation, the original data is firstly expanded, as shown in the figure below:Add two pieces of data before and after the real data. The rules for adding data have been indicated in the picture.

private val autoRunnable: Runnable = object : Runnable { override fun run() { if (mRealCount > 1 && mIsAutoPlay) { mCurPos = mCurPos % mExtendModels.size + 1 when (mCurPos) {exSecondLastPos() -> {mSelectedValid = false; Note here smoothScroll set to false, that is, there will not be a jump animation mViewPager2. SetCurrentItem (1, false) / / executed immediately, will go to the following the else Will eventually show positive article 3 of the data, Post (this)} else -> {mSelectedValid = true mviewPager2.currentitem = mCurPos postDelayed(this, AUTO_PLAY_INTERVAL) } } } } }Copy the code

The logic of infinite rotation has been stated in the comments above. For example, when VP2 slides to the sixth data (position is 5, value is a), it immediately jumps to the second data (position is 1, value is C), but the execution will continue immediately through post(this) before it is displayed. Then jump to the third data (position is 2, value is a), you can see that the data is the same as the sixth data, so as to achieve the effect of infinite rotation. When the above Runnable is set, the Handler sends a Message to start the loop:

fun startAutoPlay() {
   removeCallbacks(autoRunnable)
   postDelayed(autoRunnable, AUTO_PLAY_INTERVAL)
}
Copy the code

Above is automatically round of the implementation of the scene, in addition to the manual wheel, mainly in ViewPager2. OnPageChangeCallback# onPageScrollStateChanged (state: Int) is used in the callback to determine the next sliding position according to the position of the currentItem obtained by vp2. currentItem. The specific jump logic is the same as that of automatic rotation. One note here: State must be viewPager2.scroll_state_DRAGGING, as this value ensures that this is triggered only when finger touch slides, not when auto wheel casting.

2.2 Rotation animation transition

Mainly through LayoutManager# smoothScrollToPosition () by LinearSmoothScroller# calculateTimeForScrolling () the custom rate:

/** * 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) {/ * * * control by switch speed * / override fun calculateTimeForScrolling (dx: Int) : Int { return if (customSwitchAnimDuration ! = 0) customSwitchAnimDuration else super.calculateTimeForScrolling(dx) } } }Copy the code

2.3 Handling nested sliding conflicts

As described in the previous article, here is the code to handle sliding conflicts:

*/ 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) } } }Copy the code

Is mainly through the internal intercept method in onInterceptTouchEvent requestDisallowInterceptTouchEvent () for processing, if the internal control of nested sliding need sliding control outside the parent View without intercept events, Set to requestDisallowInterceptTouchEvent (true); Let outside the parent View to intercept events, conversely set to requestDisallowInterceptTouchEvent (false).

MotionEvent.ACTION_DOWN state must not be intercepted by the parent View, otherwise subsequent events will not be passed to the child View; In the MotionEvent.ACTION_MOVE state, the parent View will not intercept the event according to the direction and sliding distance of VP2. In the case of horizontal sliding and x-wheelbase > Y-wheelbase or vertical sliding and y-wheelbase > X-wheelbase, the parent View will not intercept the event.

2.4 Update incrementally with DiffUtil

class PageDiffUtil(private val oldModels: List<Any>, private val newModels: List<Any>) : Callback() {override fun getOldListSize(): Int = oldModels. Size /** * new data */ Override fun getNewListSize(): Int = newModels.size /** * DiffUtil call to determine whether two objects represent the same Item. True means two items are the same (which means views can be reused), false means different (which means views cannot be reused) * For example, if your items have unique ids, this method should check whether their ids are equal. */ override fun areItemsTheSame(oldItemPosition: Int, newItemPosition: Int): Boolean { return oldModels[oldItemPosition]::class.java == newModels[newItemPosition]::class.java } /** * * This method is called only if areItemsTheSame (int, int) returns true to compare whether two items have the same contents. */ override fun areContentsTheSame(oldItemPosition: Int, newItemPosition: Int): Boolean {return oldModels[oldItemPosition] == newModels[newItemPosition]} /** * AreItemsTheSame (int, int) returns true and areContentsTheSame(int, int) returns false. Override Fun getChangePayload(oldItemPosition: Int, newItemPosition: Int): Any? { return super.getChangePayload(oldItemPosition, newItemPosition) } }Copy the code

Caller:

/** * use[DiffUtil] incremental update data * @param newList new data */ fun submitList(newList: MutableList<String>) {val diffUtil = PageDiffUtil(mModels, Val diffResult = DiffUtil. CalculateDiff (DiffUtil) // / / the data to the adapter, and ultimately through the adapter. The update data diffResult notifyItemXXX. DispatchUpdatesTo (this)}Copy the code

2.5 Customizing Item Styles

We define an interface that uses two methods to create and assign an ItemView:

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

ItemView base class, which creates ImageView by default:

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 } }Copy the code

Default DefaultLoader inherits from BaseLoader and loads ImageView by Glide in display() :

/** * Default ImageView load */ 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) } }Copy the code

Of course, if you don’t want to load the ImageView, you can override it in a subclass. For example, if we want to create an ItemView that is a TextView, we can write it like this:

/** * 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 } }Copy the code

Recyclerview. Adapter is called as follows:

class MVP2Adapter : RecyclerView.Adapter<MVP2Adapter.PageViewHolder>() { override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): PageViewHolder {var itemShowView = mLoader? .createView(parent.context) return PageViewHolder(itemShowView) } override fun onBindViewHolder(holder: PageViewHolder, position: Int) {val contentStr = mModels[position] .display(holder.itemShowView.context, contentStr, holder.itemShowView) } }Copy the code

The concrete implementation is isolated through the interface, which is open for expansion and closed for modification, thus achieving the effect of opening and closing. If the caller wants to customize the Item style, he/she can implement ILoader and implement the style he/she wants.