The reason for writing this widget is that the designer gave a new navigation style of ViewPager to an app that is responsible for maintenance recently. However, he found that several commonly used navigation controls could not achieve 100% of the effect given by the designer, so he simply did his own thing.
Control only has a single java class, the code is also very simple, put it out to help people who need it.
Effect
Control provides rich configurable options. Here are two examples:
1. All configuration items use the default values (tab width package content, indicator and text width...) :
2. The tab width bisects the remaining space of the parent control, the indicator and tab width :
Configuration item
Before calling setViewPager, you can use a series of setXXX methods to set it. Chain call is supported:
indicator.setExpand(true)//Set the tab width to wrap content or divide the remaining space of the parent control equally. Default value: false, wrap content
.setIndicatorWrapText(false)//Set whether the indicator is the same width as the text or the whole tab. The default value is true, and the indicator is the same width as the text
.setIndicatorColor(Color.parseColor("#ff3300"))//indicator color
.setIndicatorHeight(2)//indicator height
.setShowUnderline(true, Color.parseColor("#dddddd"), 2)//Set whether to display underline. It is not displayed by default
.setShowDivider(true, Color.parseColor("#dddddd"), 10, 1)//Set whether to display the separator line. It is not displayed by default
.setTabTextSize(16)//Text size
.setTabTextColor(Color.parseColor("#ff999999"))//Text color
.setTabTypeface(null)//Typeface
.setTabTypefaceStyle(Typeface.NORMAL)//Font style: bold, italics, etc
.setTabBackgroundResId(0)//Set the background of the tab
.setTabPadding(0)//Set left and right padding of tab
.setSelectedTabTextSize(20)//Selected text size
.setSelectedTabTextColor(Color.parseColor("#ff3300"))//Selected text color
.setSelectedTabTypeface(null)
.setSelectedTabTypefaceStyle(Typeface.BOLD)
.setScrollOffset(120);//Roll offset
indicator.setViewPager(viewPager);
All configuration items have default values, that is to say, it is OK without any settings. Please refer to the first figure above for the effect.
Code
There are not many source codes and comments, so there is not much nonsense:
/**
* Usage:
* --Use setxxx method for style setting before calling setViewPager, supporting chain call
* --Call the setViewPager method to bind the viewPager to the simpleviewpager indicator (the adapter of the viewPager must implement the getPageTitle method)
* --Call setOnPageChangeListener to set page switch listening of viewPager
*/
public class SimpleViewpagerIndicator extends HorizontalScrollView {
//Configuration property START-----------------------------------------------------------------------------
/*
* true:Each tab width is equal to the remaining space of the parent control
* false:The width of each tab is the package content
*/
private boolean expand = false;
/*
* Indicator (short horizontal line under the selected tab)
*/
private boolean indicatorWrapText = true;//true: indicator is the same length as text; false: indicator is the same length as the whole tab
private int indicatorColor = Color.parseColor("#ff666666");
private int indicatorHeight = 2;//dp
/*
* Bottom line (indicator's background slide)
*/
private boolean showUnderline = false;//Whether to show the bottom line
private int underlineColor;
private int underlineHeight;//dp
/*
* tab Split line between
*/
private boolean showDivider = false;//Show divider or not
private int dividerColor;
private int dividerPadding;//padding,dp
private int dividerWidth;//Divider width, dp
/*
* tab
*/
private int tabTextSize = 16;//tab font size, dp
private int tabTextColor = Color.parseColor("#ff999999");//tab word color
private Typeface tabTypeface = null;//tab font
private int tabTypefaceStyle = Typeface.NORMAL;//Font style
private int tabBackgroundResId = 0;//Background resource id of each tab
private int tabPadding = 24;//Left and right inner margins of each tab, dp
/*
* Selected tab
*/
private int selectedTabTextSize = 16;//dp
private int selectedTabTextColor = Color.parseColor("#ff666666");
private Typeface selectedTabTypeface = null;
private int selectedTabTypefaceStyle = Typeface.BOLD;
/*
* scrollView Offset of overall roll, dp
*/
private int scrollOffset = 100;
//Configuration property End-------------------------------------------------------------------------------
private LinearLayout.LayoutParams wrapTabLayoutParams;
private LinearLayout.LayoutParams expandTabLayoutParams;
private Paint rectPaint;
private Paint dividerPaint;
private Paint measureTextPaint;//Brush for measuring text width
private final PageListener pageListener = new PageListener();
private OnPageChangeListener userPageListener;
private LinearLayout tabsContainer;//tab container
private ViewPager viewPager;
private int currentPosition = 0;//viewPager current page
private float currentPositionOffset = 0f;//The offset percentage of the current page of viewPager (value: 0 ~ 1)
private int selectedPosition = 0;//viewPager currently selected page
private int tabCount;
private int lastScrollX = 0;
public SimpleViewpagerIndicator(Context context) {
this(context, null);
}
public SimpleViewpagerIndicator(Context context, AttributeSet attrs) {
this(context, attrs, 0);
}
public SimpleViewpagerIndicator(Context context, AttributeSet attrs, int defStyle) {
super(context, attrs, defStyle);
setFillViewport(true);
setWillNotDraw(false);
}
public SimpleViewpagerIndicator setViewPager(ViewPager viewPager) {
this.viewPager = viewPager;
if (viewPager.getAdapter() == null) {
throw new IllegalStateException("ViewPager does not have adapter instance.");
}
viewPager.setOnPageChangeListener(pageListener);
init();
initView();
return this;
}
public SimpleViewpagerIndicator setOnPageChangeListener(OnPageChangeListener listener) {
this.userPageListener = listener;
return this;
}
private void init() {
/*
* Convert dp to px
*/
float density = getContext().getResources().getDisplayMetrics().density;
indicatorHeight = (int) (indicatorHeight * density);
underlineHeight = (int) (underlineHeight * density);
dividerPadding = (int) (dividerPadding * density);
dividerWidth = (int) (dividerWidth * density);
tabTextSize = (int) (tabTextSize * density);
tabPadding = (int) (tabPadding * density);
selectedTabTextSize = (int) (selectedTabTextSize * density);
scrollOffset = (int) (scrollOffset * density);
/*
* Container for creating tab (LinearLayout)
*/
tabsContainer = new LinearLayout(getContext());
tabsContainer.setOrientation(LinearLayout.HORIZONTAL);
tabsContainer.setLayoutParams(new LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT));
addView(tabsContainer);
/*
* Create brush
*/
rectPaint = new Paint();
rectPaint.setAntiAlias(true);
rectPaint.setStyle(Style.FILL);
dividerPaint = new Paint();
dividerPaint.setAntiAlias(true);
dividerPaint.setStrokeWidth(dividerWidth);
measureTextPaint = new Paint();
measureTextPaint.setTextSize(selectedTabTextSize);
/*
* Create two Tab's LayoutParams, one for width wrapped content and one for width equally divided parent control's remaining space
*/
wrapTabLayoutParams = new LinearLayout.LayoutParams(LayoutParams.WRAP_CONTENT, LayoutParams.MATCH_PARENT);//Width package content
expandTabLayoutParams = new LinearLayout.LayoutParams(0, LayoutParams.MATCH_PARENT, 1.0f);//Width bisection
}
private void initView() {
//Note: the meanings of currentPosition and selectedPosition are not the same. They are assigned in onPageScroll and onPageSelected respectively
//In the process of sliding from tab1 to tab2, selectedPosition will change from 1 to 2 before currentPosition
currentPosition = viewPager.getCurrentItem();
selectedPosition = viewPager.getCurrentItem();
tabsContainer.removeAllViews();
tabCount = viewPager.getAdapter().getCount();
//Create a tab and add it to the tabsContainer
for (int i = 0; i < tabCount; i++) {
addTab(i, viewPager.getAdapter().getPageTitle(i).toString());
}
//Traverse tab, set tab text size and style
updateTextStyle();
//Scroll scrollView
getViewTreeObserver().addOnGlobalLayoutListener(new OnGlobalLayoutListener() {
@Override
public void onGlobalLayout() {
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.JELLY_BEAN) {
getViewTreeObserver().removeGlobalOnLayoutListener(this);
} else {
getViewTreeObserver().removeOnGlobalLayoutListener(this);
}
scrollToChild(currentPosition, 0);//Scroll scrollView
}
});
}
/**
* Add tab
*/
private void addTab(final int position, String title) {
TextView tab = new TextView(getContext());
tab.setGravity(Gravity.CENTER);
tab.setSingleLine();
tab.setText(title);
if (tabBackgroundResId != 0) {
tab.setBackgroundResource(tabBackgroundResId);
}
tab.setPadding(tabPadding, 0, tabPadding, 0);
tab.setOnClickListener(new OnClickListener() {
@Override
public void onClick(View v) {
viewPager.setCurrentItem(position);
}
});
tabsContainer.addView(tab, position, expand ? expandTabLayoutParams : wrapTabLayoutParams);
}
/**
* Traverse tab, set tab text size and style
*/
private void updateTextStyle() {
for (int i = 0; i < tabCount; i++) {
TextView tvTab = (TextView) tabsContainer.getChildAt(i);
if (i == selectedPosition) {//Selected tab
tvTab.setTextSize(TypedValue.COMPLEX_UNIT_PX, selectedTabTextSize);
tvTab.setTypeface(selectedTabTypeface, selectedTabTypefaceStyle);
tvTab.setTextColor(selectedTabTextColor);
} else {//Unselected tab
tvTab.setTextSize(TypedValue.COMPLEX_UNIT_PX, tabTextSize);
tvTab.setTypeface(tabTypeface, tabTypefaceStyle);
tvTab.setTextColor(tabTextColor);
}
}
}
/**
* Scroll scrollView
* <p>
* Note: when the difference between the tabTextSize and the selected tabTextSize is too large, and the width mode of the tab is package content (expand = false),
* Because of the sudden change of the text width when the text selection status is switched, the tab width will suddenly change, which may cause slight jitter of scrollView when scrolling.
* Therefore, when the difference between the tabTextSize and the selected tabTextSize is too large, the tab width should be avoided to wrap the content (expand = false).
*/
private void scrollToChild(int position, int offset) {
if (tabCount == 0) return;
//getLeft():tab is relative to the parent control, that is, the left of tabsContainer
int newScrollX = tabsContainer.getChildAt(position).getLeft() + offset;
//Attach an offset to prevent the currently selected tab from leaning too far to the left
//It can be removed to see what the effect is
if (position > 0 || offset > 0) {
newScrollX -= scrollOffset;
}
if (newScrollX != lastScrollX) {
lastScrollX = newScrollX;
scrollTo(newScrollX, 0);
}
}
/**
* Draw indicator s, underline, and divider s
*/
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
if (isInEditMode() || tabCount == 0) return;
final int height = getHeight();
/*
* Draw divider s
*/
if (showDivider) {
dividerPaint.setColor(dividerColor);
for (int i = 0; i < tabCount - 1; i++) {
View tab = tabsContainer.getChildAt(i);
canvas.drawLine(tab.getRight(), dividerPadding, tab.getRight(), height - dividerPadding, dividerPaint);
}
}
/*
* Draw underline (background line of indicator)
*/
if (showUnderline) {
rectPaint.setColor(underlineColor);
canvas.drawRect(0, height - underlineHeight, tabsContainer.getWidth(), height, rectPaint);
}
/*
* Draw indicator
*/
if (indicatorWrapText) {//indicator and text equal length
rectPaint.setColor(indicatorColor);
getTextLocation(currentPosition);
float lineLeft = textLocation.left;
float lineRight = textLocation.right;
if (currentPositionOffset > 0f && currentPosition < tabCount - 1) {
getTextLocation(currentPosition + 1);
final float nextLeft = textLocation.left;
final float nextRight = textLocation.right;
lineLeft = lineLeft + (nextLeft - lineLeft) * currentPositionOffset;
lineRight = lineRight + (nextRight - lineRight) * currentPositionOffset;
}
canvas.drawRect(lineLeft, height - indicatorHeight, lineRight, height, rectPaint);
} else {//Equal length of indicator and tab
rectPaint.setColor(indicatorColor);
View currentTab = tabsContainer.getChildAt(currentPosition);
float lineLeft = currentTab.getLeft();
float lineRight = currentTab.getRight();
if (currentPositionOffset > 0f && currentPosition < tabCount - 1) {
View nextTab = tabsContainer.getChildAt(currentPosition + 1);
final float nextLeft = nextTab.getLeft();
final float nextRight = nextTab.getRight();
lineLeft = lineLeft + (nextLeft - lineLeft) * currentPositionOffset;
lineRight = lineRight + (nextRight - lineRight) * currentPositionOffset;
}
canvas.drawRect(lineLeft, height - indicatorHeight, lineRight, height, rectPaint);
}
}
/**
* Gets the left and right of the text in the specified tab
*/
private void getTextLocation(int position) {
View tab = tabsContainer.getChildAt(position);
String tabText = viewPager.getAdapter().getPageTitle(position).toString();
float textWidth = measureTextPaint.measureText(tabText);
int tabWidth = tab.getWidth();
textLocation.left = tab.getLeft() + (int) ((tabWidth - textWidth) / 2);
textLocation.right = tab.getRight() - (int) ((tabWidth - textWidth) / 2);
}
private LeftRight textLocation = new LeftRight();
class LeftRight {
int left, right;
}
private class PageListener implements OnPageChangeListener {
@Override
public void onPageScrolled(int position, float positionOffset, int positionOffsetPixels) {
currentPosition = position;
currentPositionOffset = positionOffset;
//scrollView scrolling
scrollToChild(position, (int) (positionOffset * tabsContainer.getChildAt(position).getWidth()));
invalidate();//After invalidate, onDraw will be called to draw indicator s, divider s, etc
if (userPageListener != null) {
userPageListener.onPageScrolled(position, positionOffset, positionOffsetPixels);
}
}
@Override
public void onPageScrollStateChanged(int state) {
if (state == ViewPager.SCROLL_STATE_IDLE) {
scrollToChild(viewPager.getCurrentItem(), 0);//scrollView scrolling
}
if (userPageListener != null) {
userPageListener.onPageScrollStateChanged(state);
}
}
@Override
public void onPageSelected(int position) {
selectedPosition = position;
updateTextStyle();//Update tab text size and style
if (userPageListener != null) {
userPageListener.onPageSelected(position);
}
}
}
//setter------------------------------------------------------------------------------------
......
}
For the complete code and demo, see: https://github.com/al4fun/SimpleViewpagerIndicator