自定义View指示器让指示器下划线和文字对齐
指示器的标签分为两种情形,一种是固定的一般3~5个标签,另一种的移动的标签;开发中经常会使用MD风格的TabLayout控件,这个控件什么都很好,就是指示器的下滑线和文字不能够对齐,这一点用过的同学都会知道的,我也在网上找了好久,也找到不到现成的,只好自己动手来写一个啦!
先声明一下,我是吧指示器标签分两种情形来写的,固定和不固定,没有合并,因为不是继承同一个控件;另外要多看看TabLayout的源代码,对于自定义这个控件很有好处,可以多多借鉴里面的实现;
最下面可以下载代码,有问题可以留言交流;
说了这么多先看看固定的标签的效果:(Tab是固定的效果)
这种效果实现的主要步骤思路:
因为这种标签固定
1.首先要把所有的标签文字全部放到一个容器里面,宽度按个数均分,容器选水平LinearLayout
2.要关联上一个ViewPager,并根据ViewPager的改变动态绘制指示器;
添加的代码如下:
/**
* 把数据集合设置成Tab标签
*/
public void setTabTitle(List<String> list) {
if (list == null)
return;
mTabTitleList = list;//tab数据源
mTabCount = list.size();//tab的个数
for (int i = 0; i < list.size(); i++) {
String text = list.get(i);
addView(generateText(text));//添加到容器中
}
setTabOnClickListener();//设置点击事件
}
/**
* 自动生成TextView
*/
public TextView generateText(String text) {
TextView textView = new TextView(getContext());
textView.setTextSize(TypedValue.COMPLEX_UNIT_SP, mTextSize);
textView.setTextColor(mNormalColor);
textView.setGravity(Gravity.CENTER);
textView.setText(text);
LinearLayout.LayoutParams lp = new LinearLayout.LayoutParams(0,
LinearLayout.LayoutParams.MATCH_PARENT, 1);
textView.setLayoutParams(lp);
return textView;
}
/**
* 每一个tab的点击事件
*/
public void setTabOnClickListener() {
if (mTabTitleList != null) {
for (int i = 0; i < mTabCount; i++) {
TextView textView = (TextView) getChildAt(i);
textView.setOnClickListener(new OnClickListener() {
@Override
public void onClick(View v) {
int index = indexOfChild(v);
if (mViewPager != null)
mViewPager.setCurrentItem(index, true);
}
});
}
}
}
上面代码很简单就是把根据文本生成了TextView添加到LinearLayout里面,并且均分;这一步设置好了以后,下面就开始关联ViewPager;
private RectF rectF = new RectF();//要绘制的矩形
//我们是根据下面两个参数来动态计算并绘制
private int position;//ViewPager的里面的position
private float ration;//滑动的百分比
/**
* 关联到ViewPager
*/
public void setViewPager(ViewPager viewPager) {
this.mViewPager = viewPager;
this.mViewPager.addOnPageChangeListener(new OnPageChangeListener() {
@Override
public void onPageSelected(int pos) {
}
@Override
public void onPageScrolled(int arg0, float arg1, int arg2) {
position = arg0;
ration = arg1;
//这是为了找出指示器大概会滑动到那个Tab位置
int pos = Math.round(arg0 + arg1);
for (int i = 0; i < mTabCount; i++) {
TextView textView = (TextView) getChildAt(i);
if (i == pos) {
textView.setTextColor(mSelectedColor);
} else {
textView.setTextColor(mNormalColor);
}
}
//重绘
postInvalidate();
}
@Override
public void onPageScrollStateChanged(int state) {
}
});
}
这里我们主要在onPageScrolled这个方法里面去做一下更新的操作;其中
int pos = Math.round(arg0 + arg1);这一行代码是为了让ViewPager切换到一半的是时候会显示另一个tab被选中,这一点我也是从源代码里面看到的,这里借鉴了一下;最后重新绘制;
@Override
protected void dispatchDraw(Canvas canvas) {
super.dispatchDraw(canvas);
if (mTabTitleList != null && mTabCount > 0) {
String text = mTabTitleList.get(position);//拿到文本
int textWidth = (int) mPaint.measureText(text);//测量一下宽度
int perWidth = mViewWidth / mTabCount;//每个Tab的平均宽度
rectF.set(
perWidth * (position + ration) + perWidth / 2 - textWidth / 2,
mViewHeight - mPagerIndicatorHeight,
perWidth * (position + ration) + perWidth / 2 + textWidth / 2,
mViewHeight);
canvas.drawRect(rectF, mPaint);
}
}
这里重写一下dispatchDraw方法,在super.dispatchDraw方法里面是绘制子View,等它们都绘制完毕之后,在进行指示器下面的绘制;这一步不要在onDraw方法里面绘制,因为LinearLayout本身没有什么需要绘制的,只是孩子View需要绘制;即使你绘制了也会被dispatchDraw方法绘制的内容覆盖;
固定标签的指示器到这里就就结束了,还有其他一些初始化的工作这里不说了;下面我们在来看看不固定的情形,也就是标签的个数比较多的情形;
上面的情形在写的过程中,大部分代码都是参照TabLayout的,先说一下思路吧:
因为这种标签可以移动
1.首先要把所有的标签文字全部放到一个容器里面,宽度按个数均分,容器选HorizontalScrollView,里面只能有一个View我选择LinearLayout
2.要关联上一个ViewPager,并根据ViewPager的改变动态绘制指示器;
先初始化了代码:
public ViewPagerIndicator_Scroll(Context context, AttributeSet attrs) {
super(context, attrs);
setHorizontalScrollBarEnabled(false);// Disable the Scroll Bar
initPaint();// 初始化画笔
/**
* HorizontalScrollView只能包含一个子View
*/
mLinearLayout = new LinearLayout(context);
mLinearLayout.setOrientation(LinearLayout.HORIZONTAL);// 水平方向
mLinearLayout.setLayoutParams(new HorizontalScrollView.LayoutParams(
ViewGroup.LayoutParams.MATCH_PARENT,
ViewGroup.LayoutParams.MATCH_PARENT));
addView(mLinearLayout);//添加
}
这里要注意是让setHorizontalScrollBarEnabled让滚动条不可用,而且要添加一个孩子容器,
我们把这些标签全部放到这个孩子容器里面;
生成文本标签的代码:
public TextView generateText(String text) {
TextView textView = new TextView(getContext());
textView.setTextSize(TypedValue.COMPLEX_UNIT_SP, mTextSize);
textView.setTextColor(mNormalColor);
textView.setGravity(Gravity.CENTER);
textView.setText(text);
LinearLayout.LayoutParams lp = new LinearLayout.LayoutParams(
(int) mPaint.measureText(text), //宽度最直接使用画笔测量出来的宽度
LinearLayout.LayoutParams.MATCH_PARENT);
lp.setMargins(dp2px(5), 0, dp2px(5), 0);//设置了左右间距,可以自己调整
textView.setLayoutParams(lp);
return textView;
}
宽度我没有使用Wrap_content那样会有挤压,这里直接使用画笔测量文本的宽度;
我们将生成的表面添加容器里面之后,我们最后就是和ViewPager的联动了
public void setViewPager(ViewPager viewPager) {
this.mViewPager = viewPager;
this.mViewPager.addOnPageChangeListener(new OnPageChangeListener() {
@Override
public void onPageSelected(int pos) {
}
@Override
public void onPageScrollStateChanged(int state) {
}
@Override
public void onPageScrolled(int position, float positionOffset,
int positionOffsetPixels) {
int pos = Math.round(position + positionOffset);
for (int i = 0; i < mTabCount; i++) {
TextView textView = (TextView) mLinearLayout.getChildAt(i);
if (i == pos) {
textView.setTextColor(mSelectedColor);
} else {
textView.setTextColor(mNormalColor);
}
}
setIndicatorPositionFromTabPosition(position, positionOffset);
scrollTo(calculateScrollXForTab(position, positionOffset), 0);
}
});
}
在onPageScrolled方法中思路内部是这样的:
1,设置一下目前将要滑动到那个标签,我们就并做高亮显示;
2,根据ViewPager位置信息改变绘制指示器的下划线;
3,移动当前View的内容,为了更好的展示
这三个步骤,我全部 都是参照的TabLayout的源代码;我是搞了半天怎么也行;最后才想着看看人家的代码实现;这也算是我的经验吧;直接使用了代码中的算法后,出来效果就是内容滑动的和下划线会对齐;就是这么神奇!!!贴一下代码吧:
public void setIndicatorPositionFromTabPosition(int position, float positionOffset) {
mSelectedPosition = position;
mSelectionOffset = positionOffset;
updateIndicatorPosition();
}
private void updateIndicatorPosition() {
View selectedTitle = mLinearLayout.getChildAt(mSelectedPosition);
int left, right;
if (selectedTitle != null && selectedTitle.getWidth() > 0) {
left = selectedTitle.getLeft();
right = selectedTitle.getRight();
if (mSelectionOffset > 0f && mSelectedPosition < mLinearLayout.getChildCount() - 1) {
// Draw the selection partway between the tabs
View nextTitle = mLinearLayout.getChildAt(mSelectedPosition + 1);
left = (int) (mSelectionOffset * nextTitle.getLeft() +
(1.0f - mSelectionOffset) * left);
right = (int) (mSelectionOffset * nextTitle.getRight() +
(1.0f - mSelectionOffset) * right);
}
} else {
left = right = -1;
}
setIndicatorPosition(left, right);
}
void setIndicatorPosition(int left, int right) {
if (left != mIndicatorLeft || right != mIndicatorRight) {
mIndicatorLeft = left;
mIndicatorRight = right;
ViewCompat.postInvalidateOnAnimation(this);
}
}
private int calculateScrollXForTab(int position, float positionOffset) {
View selectedChild = mLinearLayout.getChildAt(position);
View nextChild = position + 1 < getChildCount()
? getChildAt(position + 1)
: null;
int selectedWidth = selectedChild != null ? selectedChild.getWidth() : 0;
int nextWidth = nextChild != null ? nextChild.getWidth() : 0;
// base scroll amount: places center of tab in center of parent
int scrollBase = selectedChild.getLeft() + (selectedWidth / 2) - (getWidth() / 2);
// offset amount: fraction of the distance between centers of tabs
int scrollOffset = (int) ((selectedWidth + nextWidth) * 0.5f * positionOffset);
return (ViewCompat.getLayoutDirection(this) == ViewCompat.LAYOUT_DIRECTION_LTR)
? scrollBase + scrollOffset
: scrollBase - scrollOffset;
}