Out of the box source address

Onion math with BannerView

Support for XML custom attributes:

  • bv_viewHeight: Height of the Banner view area. If the value is smaller than or equal to 0, it is the height of the Banner view
  • bv_viewCornerRadius: Radius of the corner of the view area
  • bv_itemViewWidthRatio: Sets the width of the ItemView based on the percentage of the layout width
  • bv_itemViewMargin: Sets the spacing between ItemViews
  • bv_intervalInMillis: Time of Banner rotation (in SMOOTH mode is the time of Banner moving from right to left at constant speed)
  • bv_pageHoldInMillis: How long the page stays after a finger swipe (SMOOTH mode only)
  • bv_scrollMode: Sets the Banner scroll mode
    • INTERVAL: Interval switchover mode
    • SMOOTH: Uniform rolling mode
  • bv_itemViewAlignThe alignment of the ItemView with the parent WrapperView (determines the whitespace of the itemViewMargin)
    • CENTER_HORIZONTAL: Horizontal center
    • ALIGN_PARENT_LEFT: left-aligned
    • ALIGN_PARENT_RIGHT: align to the right

The exposed apis are:

  • setBannerViewImpl(impl: IBannerView): Sets the required implementation class for the Banner
  • startAutoScroll(): Start automatic scrolling (no scrolling if the number of pages is less than 1)
  • stopAutoScroll(): Stops automatic scrolling
/** * define the page switch callback */
interface OnPageChangeListener {
    fun onPageSelected(position: Int)
}

interface IBannerViewBase {
    fun getCount(a): Int

    fun getItemView(context: Context): View

    fun onBindView(itemView: View, position: Int)
}

/** * External implementation that BannerView relies on */
interface IBannerView : OnPageChangeListener.IBannerViewBase {

    /** * Default view */ when count is 0
    fun getDefaultView(context: Context): View? {
        return null
    }

    /** * Auto scrolling is disabled by default */
    fun isDefaultAutoScroll(a): Boolean {
        return false
    }

    override fun onPageSelected(position: Int){}}Copy the code

The origin of

After I took over the Banner control of our company, the story is like this:

  1. A few months ago the UI team redefined the Banner style, where the indicator was changed from the origin to a small bar, and placed some distance directly below the Banner. Business was tight, and it took more than a day to hastily wrap it in ViewPager, while the indicator was coupled to the Banner.
  2. Then, in order to squeeze out some page space, the UI boss required that, under certain conditions, the indicator bar should be placed inside the Banner, some distance from the bottom. Again, business was pressing, and AD hoc code was used to address the requirements.
  3. Finally, the business side freed up time and I refactored the BannerView to decouple the indicators, deal with the temporary code, clean up the home code, and so on.
  4. Not long after that, the UI guy said he was going to redefine the Banner style… The page switching mode has been changed from the usual ViewPager style to “moving evenly from right to left,” and “swiping fingers like a ViewPager,” and “multi-screen gallery effect,” and “spacing between pages.”
  5. So I started a trip to step on pits and growth, which taste, “really sweet”!

Take a look at the final feel:

Pit stepping process:

  1. After all, the previous ViewPager-based Banner was already packaged, and considering that “finger sliding should switch handle like ViewPager”, my first reaction was to make an extension based on the previous code, so as to cleverly avoid dealing with the problem of switching handle.
  2. Focusing on “the effect of constant movement from right to left,” I passedreflectionThe way to useLinear Scroller instancesReplaced the ViewPagermScrollerInstance, because switch to a page every five seconds, andmScrollerPerform an animation set to five seconds so it looks like “moving evenly from right to left”…
  3. Of course I also use gesture eventsDOWN UPAnd so on.mScrollerTo maintain the feel of the finger when switching…
  4. That seems to solve the dynamic effect problem. Right! It just seems to solve the problem, but when embedding a Banner like this into a list (RecyclerView), performance problems, lag bugs come!
  5. andViewPagerPoor handling of gallery effects (I don’t like clipChildren’s approach)

So:

  1. So I decided to rewrite the underlying implementation.
  2. So I went looking for a solution with the last thing I wanted to deal with: the hand-switch problem.
  3. So I open up “small plane”, open up “Chrome”, type in “RecyclerView implement ViewPager”.
  4. Then I learned that when the Jade Emperor closes a door, he will open a window for you. This window is PagerSnapHelper!

Growth summary:

  1. This is an extremely unsuccessful solution!
  2. Don’t adopt solutions that “seem to solve”!
  3. When you feel that the implementation of the scheme is not routine, not smooth, unreasonable, the probability of this scheme is not available!
  4. When you need a solution, talk to your colleagues, Google it, and you might get something.

Thinking analysis

NOTE:

  1. In this article we will focus on BannerView encapsulation and implementation, about the lower levelPagerSnapHelperThe principle part is not in the scope, but inThe article I readA link has been posted to help you make your own.

There is a long way to go. Let’s first sort out the requirements:

  1. To support two modes of scrolling, interval switching, smooth scrolling
  2. Support setting view area rounded corners
  3. Support setting ItemView rounded corners (ItemView) (this requirement is not implemented this time, it will be automatically ignored below)
  4. Support infinite loop scrolling
  5. Support setting the width of ItemView according to the ratio of the width of BannerView
  6. Support setting spacing between ItemViews
  7. To support setting the scroll interval, uniform mode to support setting the time to scroll a page
  8. To support the setting of constant speed mode, after the finger slide, the page stay time
  9. Support setting the alignment of ItemView with parent WrapperView (determines the white space of itemViewMargin)
  10. Support setting whether scrolling is enabled by default
  11. Support default views for null data sources
  12. To support data source with only 1 banner, disable scrolling
  13. The API controls automatic scrolling and pausing of the Banner to be exposed
  14. Support setting of indicators, flexible control of Indicator position, and decoupling from BannerView

🤩 So many requirements, do not be afraid, we need to deal with the core technical points:

  1. Smooth scrolling modeYou can useRecyclerView+PagerSnapHelperThe implementation,Interval roll modeYou can continue to use the ViewPager implementation, or you can use the former. (Uniformly used in this articleRecyclerView+PagerSnapHelperViewPager (ViewPager, ViewPager, ViewPager)
  2. Set rounded corners againXfermodeDo cutting synthesis. (This approach is in a previous articleShadowLayout, so this article will not repeat)
  3. The requirement [4] is to return in.max_value to getItemCount() in ADPter, and then calculate the remainder with the current position and the real count when binding View, and bind data as the real position.
  4. Requirements [4] to [13] do not have technical complexity, but business complexity, routine implementation can be done.
  5. Requirements [14] can define the interface involved with the Indicator for code decoupling and extend the BannerView from the RelativeLayout so that the Indicator can be flexibly positioned as a child View in XML.

This way, it’s just a matter of patience + time to get the BannerView we want. Below, I will pick the important points in this implementation to illustrate, as follows:

  1. RecyclerView+PagerSnapHelper implementation of PagerRecyclerView
  2. The PagerViewFactory that generates the PagerView instance
  3. Decoupling implementation of Indicator

PagerRecyclerView

See the name to know this is a RecyclerView to achieve ViewPager function class, so inherit from RecyclerView.

As the core function implementation class of BannerView, IPagerViewInstance is defined to decouple it from the upper layer (that is, it is easy to switch to other implementations, such as ViewPager implementation).

/** * PagerView function instance to implement the interface */
interface IPagerViewInstance {

    /** * Set auto scroll *@paramIntervalInMillis: Int The INTERVAL between page switching in INTERVAL mode and the time required to scroll a page in SMOOTH mode */
    fun startAutoScroll(intervalInMillis: Int)

    /** * stop automatic scrolling */
    fun stopAutoScroll(a)

    /** * Gets the position of the current Item (the index of the List) */
    fun getCurrentPosition(a): Int

    /** * Gets the current real Item position (the index of the List) */
    fun getRealCurrentPosition(realCount: Int): Int

    /** * Set whether smooth mode is enabled, otherwise interval switch mode */
    fun setSmoothMode(enabled: Boolean)

    /** * Set the page stay duration */
    fun setPageHoldInMillis(pageHoldInMillis: Int)

    /** * set page switch callback */
    fun setOnPageChangeListener(listener: OnPageChangeListener)

    /** * Notifies data refresh */
    fun notifyDataSetChanged(a)
}
Copy the code

The use of PagerSnapHelper is extremely simple, just create an instance, attachToRecyclerView, you can change RecyclerView into ViewPager. This place is amazing!! We should all pursue the ultimate design of this API.)

/** * slide to position helper */
private varmSnapHelper: PagerSnapHelper = PagerSnapHelper() ... Init {mSnapHelper. AttachToRecyclerView (omit codethis)... Omit code}Copy the code

The interval switching mode uniform rolling mode is mainly realized in startTimer() method. The difference between the two is that the interval time of Timer is different and the method of callback is different. Among them, the Timer interval of uniform mode needs to be calculated by using the external setting of the scrolling time of one screen, the width of one screen and the distance of each scrollBy.

/** * Start timer */
private fun startTimer(a){ mTimer? .cancel()if (mWidth > 0&& mFlagStartTimer && context ! =null && context is Activity) {
        mTimer = timer(initialDelay = mDelayedTime, period = mPeriodTime) {
            if (mScrollState == SCROLL_STATE_IDLE) {
                (context as Activity).runOnUiThread {
                    if (mSmoothMode) {
                        scrollBy(DEFAULT_PERIOD_SCROLL_PIXEL, 0)
                        triggerOnPageSelected()
                    } else{ smoothScrollToPosition(++mOldPosition) mPageChangeListener? .onPageSelected(mOldPosition) } } } } } }override fun onSizeChanged(w: Int, h: Int, oldw: Int, oldh: Int) {
    super.onSizeChanged(w, h, oldw, oldh)
    mWidth = (w - paddingLeft - paddingRight).toFloat()
    mHeight = (h - paddingTop - paddingBottom).toFloat()

    // Calculate the time interval of uniform rolling
    if (mSmoothMode) {
        mPeriodTime = (mSmoothSpeed / (mWidth / DEFAULT_PERIOD_SCROLL_PIXEL)).toLong()
    }

    if (mTimer == null) {
        startTimer()
    }
}
Copy the code

Page selection is based on the findSnapView method provided in PagerSnapHelper, which first finds the Snap (the current target View), and then finds its position, of course, with a variable to record, to prevent multiple callback.

/** * Trigger OnPageSelected callback */
private fun triggerOnPageSelected(a) {
    val layoutManager = getLinearLayoutManager()
    val view = mSnapHelper.findSnapView(layoutManager)
    if(view ! =null) {
        val position = layoutManager.getPosition(view)
        // Prevent the same location from firing more than once
        if(position ! = mOldPosition) { mOldPosition = position mPageChangeListener? .onPageSelected(position) } } }Copy the code

One other thing worth talking about is that you need to change the position of Snap when you initialize it, because PagerSnapHelper doesn’t work until you swipe it and RecyclerView slides out of ViewPager, so if you don’t change it when you initialize it you’ll find that the selected page is not centered, Or a RecyclerView. So how do you correct it? Here’s the PagerSnapHelper implementation, move it over and modify it slightly.

/** * Correct the position of the SnapView at first initialization */
private fun correctSnapViewPosition(a) {
    val layoutManager = getLinearLayoutManager()
    val snapView = mSnapHelper.findSnapView(layoutManager)
    if(snapView ! =null) {
        val snapDistance = mSnapHelper.calculateDistanceToFinalSnap(layoutManager, snapView)
        if(snapDistance ! =null) {
            if (snapDistance[0] != 0 || snapDistance[1] != 0) {
                // We changed the source smoothScrollBy to scrollBy, so that the correction process is not visually noticeable
                scrollBy(snapDistance[0], snapDistance[1])}// Trigger the callback for the first time
            triggerOnPageSelected()
        }
    }
}

/** * this is the source */
void snapToTargetExistingView() {
    if (this.mRecyclerView ! =null) {
        LayoutManager layoutManager = this.mRecyclerView.getLayoutManager();
        if(layoutManager ! =null) {
            View snapView = this.findSnapView(layoutManager);
            if(snapView ! =null) {
                int[] snapDistance = this.calculateDistanceToFinalSnap(layoutManager, snapView);
                if (snapDistance[0] != 0 || snapDistance[1] != 0) {
                    this.mRecyclerView.smoothScrollBy(snapDistance[0], snapDistance[1]);
                }
            }
        }
    }
}
Copy the code

The above is the key point of PagerRecyclerView I think, the other parts are the processing and implementation of business logic, we can open the source code to eat.

PagerViewFactory

The factory method pattern is used to create the underlying core implementation of the Banner.

First, we define the BannerView instance interface, which will be used as the constructor parameter of the factory instance to distinguish between creating the underlying implementation.

interface IBannerViewBase {
    fun getCount(a): Int

    fun getItemView(context: Context): View

    fun onBindView(itemView: View, position: Int)
}

/** * Define the BannerView instance interface */
interface IBannerViewInstance : IBannerViewBase {

    fun getContext(a): Context

    fun isSmoothMode(a): Boolean

    fun getItemViewWidth(a): Int

    fun getItemViewMargin(a): Int

    fun getItemViewAlign(a): Int
}
Copy the code

The factory has a getPagerView() method to create the Banner core implementation

/** * Factory creates the corresponding PagerView instance */ according to the parameter
override fun getPagerView(a): IPagerViewInstance {
    return if (bannerView.isSmoothMode()) {
        casePagerRecycler(true)}else {
        if (intervalUseViewPager) {
            // You can use ViewPager for the underlying implementation as needed
            throw IllegalStateException("There is no underlying implementation of ViewPager here.")}else {
            casePagerRecycler(false)}}}Copy the code

Here is to create a good before writing PagerRecyclerView, in fact, is to create a configuration using a RecyclerView process.

/** * Process PagerRecyclerView */
private fun casePagerRecycler(isSmoothMode: Boolean): IPagerViewInstance {
    val recyclerView = PagerRecyclerView(bannerView.getContext())
    recyclerView.layoutManager = LinearLayoutManager(bannerView.getContext(), LinearLayoutManager.HORIZONTAL, false)
    recyclerView.adapter = object : RecyclerView.Adapter<RecyclerView.ViewHolder>() {
        override fun getItemCount(a): Int {
            return Int.MAX_VALUE
        }

        override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) {
            if(! isActivityDestroyed(holder.itemView.context)) {val realPos = position % bannerView.getCount()
                bannerView.onBindView(holder.itemView.findViewById(R.id.id_real_item_view), realPos)
            }
        }

        override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder {
            val itemWrapper = LayoutInflater.from(parent.context).inflate(
                R.layout.layout_banner_item_wrapper,
                parent,
                false
            ) as RelativeLayout

            // Handle the width of ItemViewWrapper
            itemWrapper.layoutParams.width = bannerView.getItemViewWidth() + bannerView.getItemViewMargin()

            // External actual ItemView
            val itemView = bannerView.getItemView(parent.context)
            itemView.id = R.id.id_real_item_view
            val ivParams = RelativeLayout.LayoutParams(
                bannerView.getItemViewWidth(),
                ViewGroup.LayoutParams.MATCH_PARENT
            )
            ivParams.addRule(bannerView.getItemViewAlign())

            // Add ItemView to Wrapper
            itemWrapper.addView(itemView, ivParams)
            return object : RecyclerView.ViewHolder(itemWrapper) {}
        }
    }

    // Initialize the location
    recyclerView.scrollToPosition(bannerView.getCount() * 100)
    recyclerView.setSmoothMode(isSmoothMode)

    return recyclerView
}
Copy the code

Decoupling implementation of Indicator

The conventional approach to decoupling is to abstract methods to define interfaces. So we define two interfaces, one for the indicator instance to implement, and one for the external implementation on which the indicator depends. So using these two interfaces, you can customize the implementation style you want.

/** * The interface that the indicator instance needs to implement */
interface IIndicatorInstance {

    /** * sets the external implementation */
    fun setIndicator(impl: IIndicator)

    /** * rearrange */
    fun doRequestLayout(a)

    /** * redraw */
    fun doInvalidate(a)

}

/** * The external implementation on which the indicator depends */
interface IIndicator {

    /** * Get the total number of Adapters */
    fun getCount(a): Int

    /** * gets the index of the currently selected page */
    fun getCurrentIndex(a): Int

}
Copy the code

For the CrossBarIndicator we are implementing this time, it is a regular custom View, there is nothing more to say here. The important point is that there is a requirement for flexible control of indicator position, how to achieve? Our BannerView is a RelativeLayout. Indicator is a relative View that can easily control its location.

Then, take a look at the key code in BannerView:

override fun onFinishInflate(a) {
    super.onFinishInflate()
    findIndicator()
}

/** * Find the indicator */ in the child View
private fun findIndicator(a) {
    for (i in 0 until childCount) {
        val child = getChildAt(i)
        if (child is IIndicatorInstance) {
            // When the layout is filled, find the Indicator in the child View and save it
            mIndicator = child
            return}}}/** * Initializes view */
private fun initView(a) {
    if(mBannerViewImpl ! =null && mWidth > 0) {
        valbvImpl = mBannerViewImpl!! removeAllViews() ... Omit code// Initialize the indicator
        if(mIndicator ! =null) { mIndicator? .setIndicator(object : IIndicator {

                override fun getCount(a): Int {
                    return bvImpl.getCount()
                }

                override fun getCurrentIndex(a): Int {
                    return mPagerViewInstance.getRealCurrentPosition(bvImpl.getCount())
                }

            })
            // Add the indicator back
            addView(mIndicator as View)
        }
    }

}
Copy the code

At the end of the article

Here the whole to say the end, the BannerView implementation details, logic or a lot of, but the complexity is not so high, suggest eating source code, if you have questions welcome message ~ O(∩_∩)O haha ~

My personal ability is limited. If there is something wrong, I welcome you to criticize and point it out. I will accept it humbly and modify it in the first time so as not to mislead you.

Read the article

  • SnapHelper core tutorial

My other articles

  • [Custom View] Douyin popular text clock – Part 1
  • 【 custom View】 onion math same style ShadowLayout -ShadowLayout
  • 【 custom View】 Onion math with the same radar map in-depth analysis -RadarView
  • 【 custom View】 Onion mathematics with the same Banner evolution -BannerView