Automatic Rotary ViewPager with Indicators
Preface
Auto-Rotary ViewPager has a wide range of applications, which can be applied to a variety of internal applications, most importantly for display advertising. There are several sub-views inside, which automatically slide into the next view at intervals, and the indicator at the bottom changes accordingly. That's what we're going to do.
This paper mainly refers to the following articles: http://blog.csdn.net/a553181867/article/details/52734261#comments
Principle analysis
First of all, let's consider that the ViewPager that comes with the system is a stand-alone control. It has neither indicator nor auto-scrolling function, but it is an off-the-shelf control that can slide left and right. So we definitely need to be based on the ViewPager. The configuration we need to do is no different from the general ViewPager. It is to get an instance, create an adapter, and set up properly. Orchestration.
Therefore, we can use a layout to wrap the ViewPager and put indicators in it.
So, the first step is to create a new java class named BannerViewPager, which inherits from FrameLayout, where I use frame layout, and of course other layouts that can achieve the same effect.
public class BannerViewPager extends FrameLayout {
private Contetxt mContext;
private ViewPager mViewPager;
public BannerViewPager(Context context) {
this(context, null);
}
public BannerViewPager(Context context, AttributeSet attrs) {
this(context, attrs, 0);
}
public BannerViewPager(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
this.mContext = context;
initViews();
}
public 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);
}
}
The main idea here is to initialize the ViewPager and set layout parameters to get the correct display in FrameLayout. Then we need to scroll the ViewPager automatically. The principle is not difficult. We just need to know the sliding state of the ViewPager and the current page position. ViewPager has such a monitor: ViewPager.OnPageChangeListene, which calls back the following methods of the monitor whenever the ViewPager slides:
public interface OnPageChangeListener {
//As long as the ViewPager slides, the method calls back
public void onPageScrolled(int position, float positionOffset, int positionOffsetPixels);
//Callback when the current page is selected
public void onPageSelected(int position);
//Callback when ViewPager's status changes
public void onPageScrollStateChanged(int state);
}
So, we just set up listeners for the ViewPager (calling the addOnPageChangeListener method) and rewrite these methods to meet our requirements, so we need to let the BannerViewPager implement the ViewPager.OnPageChangeListener interface:
public class BannerViewPager extends FrameLayout implements ViewPager.OnPageChangeListener {
...
//Save the current position value
private int mCurrentPosition;
//viewpager's rolling state
private int mViewPagerScrollState;
private int mReleasingTime = 0;
...
@Override
public void onPageScrolled(int position, float positionOffset, int positionOffsetPixels) {
int max = mAdapter.getCount() - 1;
int pos = position;
mCurrentPosition = position;
if (position == 0) {
mCurrentPosition = max - 1;
} else if (position == max) {
mCurrentPosition = 1;
}
pos = mCurrentPosition - 1;
}
@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 scroll stops, change the viewpager to make circle
mViewPager.setCurrentItem(mCurrentPosition, false);
}
}
}
Whenever the current page is selected, the onPageSelected method is called to save the current position value. So, what is the current page selected? After experimental verification, when an Item is fully displayed in the ViewPager, it is the selected state, but if it is currently being dragged by a finger, even if the next item slides to the middle position, it is not the selected state.
Next, let's look at the onPageScrollStateChanged method, which triggers when the state of the ViewPager changes. So what does ViewPager mean by state change? ViewPager has the following three states: IDLE, stop state, no finger touch; DRAGGING, being dragged by finger; SETTLING, when loosening finger, ViewPager slides to the next item in the direction of finger sliding due to inertia. In our rewrite method, mViewPageScrollState records the real-time state of the ViewPager, while stopping the state, it also records a mReleasing Time value, which will be introduced later.
Finally, let's look at the onPageScrolled method, which is called when the page is sliding and will be called all the time until the sliding is stopped. This method is used as the basis of the loop. First we get the number of items in the adapter, and then we set the current position by judging the current position. Now this method seems strange. Why should we change the position to the penultimate when it is the tenth? This is because we have to do a little bit of cycling. For example, we have three items of ABC to show. According to the logic of cycling, A will go back to C in the front and C will come to A in the back, so we can set it as a CABC A cycle form. So, when the current position is zero, in this case, the first C, we secretly change it to the penultimate C, that is, the second C; similarly, the treatment of A is the same. In the onPage Scrolled method, we already know that it is called when the page is sliding, and our transposition should be done when the page stops, so in the onPage Scroll State Changed method, if we judge that the state is stopping, we add a last sentence to set the position of the ViewPager.
By implementing this listener, we get two values of mCurrentPosition and mViewPageScrollState as the basis for automatic scrolling.
Next, we need to consider automated tasks. In Android, automated tasks can be implemented using Handler and Runnable, and loops can be continuously implemented by postDelay method. The code is as follows:
...
//the interval between rollings
private int mAutoRollingTime = 4000;
private static final int MESSAGE_AUTO_ROLLING = 0X1001;
private static final int MESSAGE_AUTO_ROLLING_CANCEL = 0X1002;
...
private Handler mHandler = new Handler(){
@Override
public void handleMessage(Message msg) {
switch (msg.what){
case MESSAGE_AUTO_ROLLING:
if(mCurrentPosition == mAdapter.getCount() - 2){
mViewPager.setCurrentItem(1,true);
}else {
mViewPager.setCurrentItem(mCurrentPosition + 1,true);
}
postDelayed(mAutoRollingTask,mAutoRollingTime);
break;
case MESSAGE_AUTO_ROLLING_CANCEL:
postDelayed(mAutoRollingTask,mAutoRollingTime);
break;
}
}
};
/**
* This runnable decides the viewpager should roll to 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);
}else {
mHandler.sendEmptyMessage(MESSAGE_AUTO_ROLLING_CANCEL);
}
}else if(mViewPagerScrollState == ViewPager.SCROLL_STATE_DRAGGING){
mHandler.sendEmptyMessage(MESSAGE_AUTO_ROLLING_CANCEL);
}
}
};
In the Runnable mAutoRolling Task, we decide whether to let the ViewPager scroll to the next page or wait according to different mViewPager ScrollState, because if the user is currently touching the ViewPager, then it must not automatically scroll to the next page. In addition, there is another case, when the user's finger leaves the screen, it will take some time to open. Start scrolling tasks automatically, otherwise it will create a bad user experience, which is what mReleasing Time does. In Hnadler, different operations are performed according to different messages sent by Runnable. If you need to scroll to the next page, you call the setCurrentItem method to slide. Here we should pay attention to the special processing we do for the loop.
Implementing Indicators
Indicators have the following requirements: Indicators are made up of a series of dots. Generally, indicators are distinguished by different colors. Here I use different shapes. The dots corresponding to the selected Page will be displayed as square, and the square can slide along with the sliding of the Page.
So, we can do this: white dots as the background of Indicator are drawn by onDraw() method, while square is displayed by a sub-View, and its position is controlled by onLayout() method, so that the effect of square moving on the dot can be realized. For their specific position control, the onPage Scrolled method above can be used to obtain the specific position and the percentage of position offset.
First, we implement the drawing part. New ViewPagerIndivactor.java inherits from LinearLayout. First, we initialize the attributes:
public class ViewPagerIndicator extends LinearLayout {
private Context mContext;
private Paint mPaint;
private View mMoveView;
private int mCurrentPosition = 0;
private float mPositionOffset;
private int mPadding = 10;
private int mRadius = 10;
private int mMoveRadius = 15;
private int mDistanceBtwItem = mRadius * 2 + mPadding;
private int mItemCount = 5;
public ViewPagerIndicator(Context context) {
this(context, null);
}
public ViewPagerIndicator(Context context, AttributeSet attrs) {
this(context, attrs, 0);
}
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.WHITE);
mMoveView = new MoveView(mContext);
addView(mMoveView);
}
public void setItemCount(int count) {
this.mItemCount = count;
requestLayout();
}
public void setRadius(int radius) {
this.mRadius = radius;
this.mMoveRadius = radius * 3 / 2;
this.mDistanceBtwItem = mRadius * 2 + mPadding;
requestLayout();
}
public void setPadding(int padding) {
this.mPadding = padding;
this.mDistanceBtwItem = mRadius * 2 + mPadding;
requestLayout();
}
public void setPositionAndOffset(int position, float offset) {
this.mCurrentPosition = position;
this.mPositionOffset = offset;
requestLayout();
}
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
setMeasuredDimension(mPadding + (mRadius * 2 + mPadding) * (mItemCount - 2),
2*mRadius + 2*mPadding);
}
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
for (int i = 0; i < mItemCount - 2; i++) {
canvas.drawCircle(mRadius + mPadding + mRadius * i * 2 + mPadding * i,
mRadius + mPadding, mRadius, mPaint);
}
}
private class MoveView extends View {
private Paint mPaint;
public MoveView(Context context) {
super(context);
mPaint = new Paint();
mPaint.setAntiAlias(true);
mPaint.setColor(Color.WHITE);
}
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
setMeasuredDimension(mMoveRadius * 2, mMoveRadius * 2);
}
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
canvas.drawCircle(mRadius, mRadius, mMoveRadius, mPaint);
}
}
}
As you can see from the above code, in the init() method, we call the setWillNotDraw(false) method. What's the use of this method? If you have written a custom View, you should know that the ViewGroup will not call its own onDraw() method by default, only if the method is set to false or a background color is set to the ViewGroup.
After solving this problem, let's look at the onMeasure() method. In this method, we need to measure the width of the indicator so as to make the next layout and drawing process. For our needs, as long as the layout can encapsulate our indicator and leave some space on the four sides, then the width of the layout is related to the number of Page s. For convenience, a default value is given here, but notice that although it's 5, there are actually two that don't need to be displayed, so only three dots are drawn afterwards.
Let's look at the onDraw() method, which draws circles according to the number of mItemCount s inside. There's nothing to say here, just notice the distance between them. Next, we draw the square, create a new inner class, inherit from View, and use onMeasure and onDraw methods to measure and draw the process, but the size has changed.
Now that the drawing part is complete, the next step is to move the MoveView. To make MoveView slide with Page sliding, we need the specific location and position offset of the Page. These two values are actually obtained inside the BannerView Pager, so we can adjust them in the BannerView Pager every time we call the onPageScrolled method. Using a method of our ViewPager Indicator, and within this method, to request layout, you can achieve the effect of MoveView sliding along with Page, as follows:
public class ViewPagerIndicator extends LinearLayout {
...
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);
}
}
public class BannerViewPager extends FrameLayout implements ViewPager.OnPageChangeListener {
...
private ViewPagerIndicator mIndicator;
...
private void initViews() {
...
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);
}
...
private void setIndicator(int position, float offset) {
mIndicator.setPositionAndOffset(position, offset);
}
@Override
public void onPageScrolled(int position, float positionOffset, int positionOffsetPixels) {
...
setIndicator(pos, positionOffset);
}
}
The request Layout () method is called in the setPositionAndOffset method, which results in the measurement, layout and redrawing of the View tree. Therefore, in the onLayout method, the location of the MoveView can be controlled by the values of mCurrentPosition and mPositionOffset.
Then add the corresponding code to the BannerViewPager to add the indicator.
Okay, so far, ViewPager Indicator has basically been completed, but there's another problem. If the data in the adapter is refreshed, the number of page s increases, while the number of indicators remains unchanged. Therefore, we must notify Indicator in time to increase the number of indicators when the data is refreshed. But let's think about it further. If ViewPager Indicator wants to get data, it needs a reference from the Adapter. Or if the Adapter needs a reference from the ViewPager Indicator, it can notify it. If it does, it's equivalent to connecting two classes that are not very relevant and coupling is too high, which is not conducive. Later maintenance. So here we use the observer pattern to implement the requirement of notifying ViewPager Indicator when adapter data refreshes. First, create two new interfaces, one is DataSetSubscriber, the observer; the other is DataSetSubject, the observer.
public interface DataSetSubscriber {
void update(int count);
}
public interface DataSetSubject {
void registerSubscriber(DataSetSubscriber subscriber);
void removeSubscriber(DataSetSubscriber subscriber);
void notifySubscriber();
}
The idea here is to implement a DataSet Subscriber in Banner ViewPager, a DataSetSubscriber in ViewPager Adaper, and a Register Subscriber to register. When the data list of ViewPage Adapter changes, call back the update() method of DataSetSubscriber and pass the current data length as a parameter. Come in, and BannerViewPager calls the ViewPager Indicator method further to reposition.
public class ViewPagerAdapter extends PagerAdapter implements DataSetSubject {
private List<DataSetSubscriber> mSubscribers = new ArrayList<>();
private List<? extends View> mDataViews;
private OnPageClickListener mOnPageClickListener;
public ViewPagerAdapter(List<? extends View> mDataViews, OnPageClickListener listener) {
this.mDataViews = mDataViews;
this.mOnPageClickListener = listener;
}
public View getView(int location) {
return this.mDataViews.get(location);
}
@Override
public int getCount() {
return mDataViews.size();
}
@Override
public boolean isViewFromObject(View view, Object object) {
return view == object;
}
@Override
public Object 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;
}
@Override
public void destroyItem(ViewGroup container, int position, Object object) {
container.removeView((View) object);
}
@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());
}
}
}
Within the update() method, the setItemCount method of ViewPagerIndicator is called to reposition.
Then the indicator is finished.
Implementing Page Click Event Processing
If you write directly according to the above code, you will find that it will report errors. There is a class that we did not write, and that class is actually to achieve the click event processing requirements.
Because often the content of ViewPager is only a general content, in order to get more detailed information, users usually click on its item to open a new page, which requires us to handle the click event. Here we define a new interface: OnPageClickListener and an onPageClick method. As follows:
public interface OnPageClickListener {
void onPageClick(View view,int position);
}
Just set a View.OnClickListener for each item view and call our onPageClick method in the onClick method when the item is initialized.
Finally, we can configure the adapter in the BannerViewPager.
public class BannerViewPager extends FrameLayout implements ViewPager.OnPageChangeListener {
...
private ViewPagerAdapter mAdapet;
...
//by default,auto-rolling is on.
private boolean isAutoRolling = true;
...
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);
// skip the first one.
mViewPager.setCurrentItem(1, false);
//start the auto-rolling task if needed
if(isAutoRolling){ postDelayed(mAutoRollingTask,mAutoRollingTime);
}
}
}
When building an adapter, you can implement OnPage ClickListener at the same time.
//Get an instance of BannerViewPager
bannerViewPager = (BannerViewPager) findViewById(R.id.banner);
//Instantiate ViewPager Adapter, 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);
}
});
//Setting up the adapter
bannerViewPager.setAdapter(mAdapter);
M View s are the corresponding set of views prepared beforehand.
This is the whole content of this paper.