English | 简体中文 | 繁體中文 | Русский язык | Français | Español | Português | Deutsch | 日本語 | 한국어 | Italiano | بالعربية
Preface
While working on a project recently, there was a requirement to implement an automatic carousel ViewPager. The most intuitive example is the ViewPager at the top of Zhihu Daily, which has several subviews that automatically slide to the next item view every few seconds, and the bottom indicator changes accordingly. The advantage of using this ViewPager is that it can display a variety of information within a limited space. Carousel ViewPager is widely used in various applications for displaying advertisements, etc. With the aim of learning and sharing, the author has written a carousel ViewPager as an independent control for future use.
Effect demonstration
Without further ado, let's first take a look at how the effect is implemented:
From the above dynamic image, we can see that when we drag the ViewPager with our fingers, the indicator below moves with the page sliding, when we click the button to add data, the data items of the ViewPager increase, and the indicator below also changes, adapting to the number of data items.
From the above dynamic image, we can see that when we do not use our fingers to drag, this ViewPager will scroll every4scrolls automatically at a speed of s left and right, when it scrolls to the last item view, the next time it will roll back to the first position.
GitHub address and usage introduction
Readers can directly get the source code from my GitHub.
GitHub:BannerViewPager,the control and its related files are placed in the library module under this directory, while the app module is a simple application for the effect demonstration above.
By the following steps, you can easily use the control:
1、Like a normal ViewPager, place the control in the layout file as follows:
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:tools="http://schemas.android.com/tools" android:layout_width="match_parent" android:layout_height="match_parent" android:orientation="vertical"> <com.chenyu.library.bannerViewPager.BannerViewPager android:id="@"+id/banner" android:layout_width="match_parent" android:layout_height="200dp"> </com.chenyu.library.bannerViewPager.BannerViewPager> <!-- others --> </LinearLayout>
2、Get an instance of BannerViewPager and make the corresponding configurations, such as when we use ViewPager, we also need to set its adapter, etc. Here, the author implements a ViewPagerAdapter, which is used as the adapter for BannerViewPager:
//Get an instance of BannerViewPager bannerViewPager = (BannerViewPager) findViewById(R.id.banner); //Instantiate ViewPagerAdapter, the first parameter is the View collection, and the second parameter is the page click listener mAdapter = new ViewPagerAdapter(mViews, new OnPageClickListener() { @Override public void onPageClick(View view, int position) { Log.d("cylog","position:")+position); } }); //Set the adapter bannerViewPager.setAdapter(mAdapter);
It is similar to the general ViewPager: get an instance - create an adapter - set the adapter. The data set of the adapter is usually a collection of View, used as the item view of ViewPager, so it is necessary to prepare the corresponding View collection in advance. In addition, the general carousel ViewPager will open the corresponding page after clicking an item, so an OnPageClickListener listener is provided here, and the listener can be created at the same time as the adapter is created.
Principle Analysis
Next, the author will briefly analyze the implementation ideas of BannerViewPager, and for the specific details, please refer to the source code~
Implement automatic scrolling
Firstly, let's think about it, the system's built-in ViewPager is an independent control, without an indicator and without automatic scrolling functionality, but it is a ready-made control that can be swiped left and right. We definitely need ViewPager, so we can use a layout to wrap ViewPager, and at the same time, put the indicator (indicator) in this layout.
Then, the first step is to create a new BannerViewPager.java that inherits from FrameLayout, and this FrameLayout has two child elements: ViewPager and indicator. As for the indicator, it will be discussed later. Inside the constructor, initialize these two controls first:
public class BannerViewPager extends FrameLayout implements ViewPager.OnPageChangeListener { private ViewPager mViewPager; private ViewPagerIndicator mIndicator; private ViewPagerAdapter mAdapter; //... public BannerViewPager(Context context, AttributeSet attrs, int defStyleAttr) { super(context, attrs, defStyleAttr); this.mContext = context; initViews(); } private void initViews() { //initialize the viewpager mViewPager = new ViewPager(mContext); ViewPager.LayoutParams lp = new ViewPager.LayoutParams(); lp.width = ViewPager.LayoutParams.MATCH_PARENT; lp.height = ViewPager.LayoutParams.MATCH_PARENT; mViewPager.setLayoutParams(lp); //initialize the indicator mIndicator = new ViewPagerIndicator(mContext); FrameLayout.LayoutParams indicatorlp = new FrameLayout.LayoutParams(LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT); indicatorlp.gravity = Gravity.BOTTOM | Gravity.CENTER; indicatorlp.bottomMargin = 20; mIndicator.setLayoutParams(indicatorlp); } //Omitted... }
There is nothing much to say here, mainly to initialize ViewPager and ViewPagerIndicator, set their layout parameters so that they can be displayed correctly in FrameLayout.
Then, implement automatic scrolling for ViewPager, the principle of its implementation is not difficult, we just need to know the sliding status of ViewPager at every moment and the current page position value, and ViewPager has such a listener: ViewPager.OnPageChangeListener, as long as ViewPager performs a slide, it will call back the following methods of this listener:
public interface OnPageChangeListener { //This method will be called back as long as the ViewPager is scrolled public void onPageScrolled(int position, float positionOffset, int positionOffsetPixels); //Callback when the current page is selected public void onPageSelected(int position); //Callback when the ViewPager state changes public void onPageScrollStateChanged(int state); }
Then, we set a listener for the ViewPager (Call the addOnPageChangeListener method) and override these methods to achieve our needs:
//Save the current position value private int mCurrentPosition; //viewpager's rolling state private int mViewPagerScrollState; @Override public void onPageScrolled(int position, float positionOffset, int positionOffsetPixels) { setIndicator(position,positionOffset); //The following will be discussed } @Override public void onPageSelected(int position) { mCurrentPosition = position; } @Override public void onPageScrollStateChanged(int state) { if(state == ViewPager.SCROLL_STATE_DRAGGING){ mViewPagerScrollState = ViewPager.SCROLL_STATE_DRAGGING; else if(state == ViewPager.SCROLL_STATE_IDLE){ mReleasingTime = (int) System.currentTimeMillis(); mViewPagerScrollState = ViewPager.SCROLL_STATE_IDLE; } }
When the current page is selected, onPageSelected method is called, and at this time, the current position value is saved. So, what is the current page being selected? According to experimental verification, when an item is completely displayed in the ViewPager, it is in the selected state, but if it is being dragged by a finger, even if the next item is sliding to the middle position, it is not in the selected state. Then, let's look at onPageScrollStateChanged method, which is triggered when the state of ViewPager changes. So,**What does the state change of ViewPager mean?**ViewPager has the following three states: IDLE, stop state, without finger touch; DRAGGING, being dragged by a finger; SETTLING, when the finger is released, the ViewPager slides to the last position it can reach due to inertia. In the methods we override, mViewPageSrollState records the real-time state of ViewPager, and when the stop state occurs, it also records a mReleasingTime value, which will be introduced later. Through this listener, we obtain the values of mCurrentPosition and mViewPageScrollState.
Next, we need to consider the issue of automatic tasks. In Android, automatic tasks can be implemented using Handler and Runnable, achieving cyclic execution through the postDelay method, as shown in the following code:
private Handler mHandler = new Handler(){ @Override public void handleMessage(Message msg) { switch (msg.what){ case MESSAGE_AUTO_ROLLING: if(mCurrentPosition == mAdapter.getCount() - 1{ mViewPager.setCurrentItem(0,true); } mViewPager.setCurrentItem(mCurrentPosition + 1,true); } postDelayed(mAutoRollingTask, mAutoRollingTime); break; case MESSAGE_AUTO_ROLLING_CANCEL: postDelayed(mAutoRollingTask, mAutoRollingTime); break; } } }; /** * This runnable decides whether the viewpager should roll to the next page or wait. */ private Runnable mAutoRollingTask = new Runnable() { @Override public void run() { int now = (int) System.currentTimeMillis(); int timediff = mAutoRollingTime; if(mReleasingTime != 0){ timediff = now - mReleasingTime; } if(mViewPagerScrollState == ViewPager.SCROLL_STATE_IDLE){ //if user's finger just left the screen, we should wait for a while. if(timediff >= mAutoRollingTime * 0.8{ mHandler.sendEmptyMessage(MESSAGE_AUTO_ROLLING); } mHandler.sendEmptyMessage(MESSAGE_AUTO_ROLLING_CANCEL); } }else if(mViewPagerScrollState == ViewPager.SCROLL_STATE_DRAGGING){ mHandler.sendEmptyMessage(MESSAGE_AUTO_ROLLING_CANCEL); } } };
Within the mAutoRollingTask Runnable, we decide whether to let the ViewPager scroll to the next page or wait based on the different mViewPagerScrollState values. Because if the user is currently touching the ViewPage, it is definitely not allowed to automatically scroll to the next page. Moreover, there is another situation where, when the user's finger leaves the screen, we need to wait for a while before starting the automatic scrolling task to avoid a poor user experience, which is where the mReleasingTime comes into play. In the Handler, different operations are performed based on the different information sent by the Runnable. If it is necessary to scroll to the next page, the ViewPager#setCurrentItem method is called to perform the slide, which has two parameters: the first parameter is the position to slide to, and the second parameter indicates whether to enable the animation.
Implement the indicator
Next, let's consider how to implement the indicator. The indicator has the following requirements: The indicator is composed of a series of dots, the dots corresponding to the unselected Page are gray, and the dots corresponding to the selected Page are orange. The orange dots can move with the sliding of the Page. When the data of ViewPage changes, such as adding a page, the dots contained in the indicator will also increase accordingly.
Then, we can implement the requirement like this: Gray dots as the background of the Indicator, drawn through the onDraw() method, and the orange dots are displayed through a child View, controlled by the onLayout() method to control its position, so that the effect of the orange dot moving on the gray dot can be achieved. The specific position control can be achieved by using the ViewPager.OnPageChangeListener#onPageScrolled method to obtain the specific position and position offset percentage.
Let's implement the drawing part first. Create a new ViewPagerIndicator.java that inherits from LinearLayout, and initialize the properties first:
public class ViewPagerIndicator extends LinearLayout { private Context mContext; private Paint mPaint; private View mMoveView; //Omitted... public ViewPagerIndicator(Context context, AttributeSet attrs, int defStyleAttr) { super(context, attrs, defStyleAttr); this.mContext = context; init(); } private void init() { //setOrientation(LinearLayout.HORIZONTAL); setWillNotDraw(false); mPaint = new Paint(); mPaint.setAntiAlias(true); mPaint.setColor(Color.GRAY); mMoveView = new MoveView(mContext); addView(mMoveView); } @Override protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { super.onMeasure(widthMeasureSpec, heightMeasureSpec); setMeasuredDimension(mPadding + (mRadius*2 + mPadding) * mItemCount,2*mRadius + 2*mPadding); } @Override protected void onDraw(Canvas canvas) { super.onDraw(canvas); for(int i = 0;i < mItemCount;i++{ canvas.drawCircle(mRadius + mPadding + mRadius * i *2 + mPadding * i, mRadius + mPadding,mRadius,mPaint); } } //Omitted... private class MoveView extends View { private Paint mPaint; public MoveView(Context context) { super(context); mPaint = new Paint(); mPaint.setAntiAlias(true); mPaint.setColor(Color.argb(255,255,176,93)) } @Override protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { super.onMeasure(widthMeasureSpec, heightMeasureSpec); setMeasuredDimension(mRadius*2,mRadius*2); } @Override protected void onDraw(Canvas canvas) { super.onDraw(canvas); canvas.drawCircle(mRadius, mRadius, mRadius, mPaint); } } }
From the above code, we can see that in the init() method, we called the setWillNotDraw(false) method. What is the use of this method? Readers who have written custom Views should know that ViewGroup does not call its own onDraw() method by default. The onDraw() method is only called when the method is set to false or a background color is set for ViewGroup.
After solving this problem, let's look at the onMeasure() method. Inside this method, we need to measure the width and height of this indicator so that the subsequent layout and drawing process can proceed. For our needs, as long as the layout can enclose our indicator and leave some space around the four sides, the width of the layout will be related to the number of Pages. For convenience, let's set a default value first, for example5Page, then corresponding5a gray dot.
We continue to look at the onDraw() method. Inside this method, the drawing of circles is performed based on the number of mItemCount. There is nothing much to discuss here; just pay attention to the distance between them.
Next, let's draw the orange dots. Create a new inner class that inherits from View and goes through the measurement and drawing process through onMeasure and onDraw methods, but with a change in color.
Alright, the drawing part is done. Next, we need to make the MoveView move. Since we need to make the MoveView move along with the sliding of the Page, we need the specific position and offset of the Page. These two values are obtained internally in BannerViewPager. Therefore, we can call a method of our ViewPagerIndicator every time onPageScrolled is called in BannerViewPager. Within this method, we can request a layout, which will enable the MoveView to move along with the sliding of the Page, as follows:
public class ViewPagerIndicator extends LinearLayout { //The above is omitted... public void setPositionAndOffset(int position,float offset){ this.mCurrentPosition = position; this.mPositionOffset = offset; requestLayout(); } @Override protected void onLayout(boolean changed, int l, int t, int r, int b) { super.onLayout(changed, l, t, r, b); mMoveView.layout( (int) (mPadding + mDistanceBtwItem * (mCurrentPosition + mPositionOffset) ), mPadding, (int) (mDistanceBtwItem * ( 1 + mCurrentPosition + mPositionOffset) ), mPadding+mRadius*2); } }
Within the setPositionAndOffset method, the requestLayout() method is called, which triggers the measurement, layout, and redrawing process of the View tree. Therefore, within the onLayout method, the position of MoveView can be controlled by using the values of mCurrentPosition and mPositionOffset.
Alright, up to now, ViewPagerIndicator has basically been completed. However, there is still one problem. If the data in the adapter is refreshed and the number of pages increases, but the number of indicators remains unchanged, the mItemCount we used above is the default value, for5Therefore, we must notify the Indicator in time when the data is refreshed to increase the number of indicators. However, if we think further, the data list is stored in the Adapter. If ViewPagerIndicator wants to get the data, it needs to get a reference to the Adapter, or the Adapter needs to get a reference to ViewPagerIndicator to be able to notify it. If this is done, it is equivalent to linking two classes that are not highly related together, which leads to a high degree of coupling. This is not conducive to future maintenance.
Therefore, here the author adopts the observer pattern to implement the requirement of notifying ViewPagerIndicator when Adapter data is refreshed. First, create two interfaces, one is DataSetSubscriber, the observer; and the other is DataSetSubject, the subject to be observed.
public interface DataSetSubscriber { void update(int count); } public interface DataSetSubject { void registerSubscriber(DataSetSubscriber subscriber); void removeSubscriber(DataSetSubscriber subscriber); void notifySubscriber(); }
The implementation idea is as follows: Implement a DataSetSubscriber(observer) inside BannerViewPager, and implement a DataSetSubject(subject to be observed) inside ViewPageAdapter. Register them through the registerSubscriber method. When the data list of ViewPageAdapter changes, it calls the update() method of DataSetSubscriber and passes the current data length as a parameter. Then BannerViewPager further calls the method of ViewPagerIndicator to rearrange the layout.
Let's take a look at ViewPagerIndicator.java:
public class ViewPagerAdapter extends PagerAdapter implements DataSetSubject { private List<DataSetSubscriber> mSubscribers = new ArrayList<>(); private List<? extends View> mDataViews; private OnPageClickListener mOnPageClickListener; /** * Constructor * @param mDataViews view list */ public ViewPagerAdapter(List<? extends View> mDataViews, OnPageClickListener listener) { this.mDataViews = mDataViews; this.mOnPageClickListener = listener; } //Omitted... @Override public void notifyDataSetChanged() { super.notifyDataSetChanged(); notifySubscriber(); } @Override public void registerSubscriber(DataSetSubscriber subscriber) { mSubscribers.add(subscriber); } @Override public void removeSubscriber(DataSetSubscriber subscriber) { mSubscribers.remove(subscriber); } @Override public void notifySubscriber() { for(DataSetSubscriber subscriber : mSubscribers) { subscriber.update(getCount()); } } ```
Since changes in the data list usually trigger the call to notifyDataSetChanged() method, we can simply call notifySubscriber() method within this method. In BannerViewPager, implementing the update() method of DataSetSubscriber is sufficient, as shown below:
```java public void setAdapter(ViewPagerAdapter adapter){ mViewPager.setAdapter(adapter); mViewPager.addOnPageChangeListener(this); mAdapter = adapter; mAdapter.registerSubscriber(new DataSetSubscriber() { @Override public void update(int count) { mIndicator.setItemCount(count); } }); //Add the viewpager and the indicator to the container. addView(mViewPager); addView(mIndicator); //start the auto-rolling task if needed if(isAutoRolling){ postDelayed(mAutoRollingTask, mAutoRollingTime); } }
In the update() method, ViewPagerIndicator#setItemCount method is called to rearrange the layout.
Then, the indicator is also completed.
Implement the click event handling of Page
There is one last requirement, which is to handle the click event of the Page, because the content of ViewPager is often just an overview. To get more detailed information, users usually click on its item to open a new page, so we need to handle the click event. In fact, the implementation is not difficult, and the idea is similar to the way the author handles click events in the related articles about RecyclerView. By defining a new interface: OnPageClickListener, define an onPageClick method. As follows:
public interface OnPageClickListener { void onPageClick(View view, int position); }
As long as you set a View.OnClickListener for each item view during the initialization of the item view, and call our onPageClick method in the onClick method, it can be done.
Specifically as shown below, ViewPagerAdapter:
@Override public View instantiateItem(ViewGroup container, int position) { View view = mDataViews.get(position); final int i = position; if(mOnPageClickListener != null){ view.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { mOnPageClickListener.onPageClick(v,i); } }); } container.addView(view); return view; }
Implement OnPageClickListener at the same time when building the adapter.
That's all for this article, thank you very much for your reading~
Welcome toGitHubSource code of this article can be obtained here, welcome to star or fork. Thank you again!
That's all for this article, I hope it will be helpful to your study, and I also hope that everyone will support the呐喊 tutorial more.
Declaration: The content of this article is from the Internet, the copyright belongs to the original author, the content is contributed and uploaded by Internet users spontaneously, this website does not own the copyright, does not edit the content manually, nor bear relevant legal liabilities. If you find any content suspected of copyright infringement, please feel free to send an email to: notice#w3If you find any copyright infringement, please send an email to notice#w (replace # with @) for reporting, and provide relevant evidence. Once verified, this site will immediately delete the content suspected of infringement.