坚持原创,原文链接

首先看一下微博的效果图:

再看一下我们的最终效果图:

仔细观察微博的效果:

  • 关注页面滑到页面的一半宽度以上时会自动切换到热门页面。
  • 看黄条的长度。当关注页面滑动一半时,黄条的长度到达“热门”两个字的接近右边,不会变长。反之,亦然。
  • 选中的页面的字体颜色有变化。
  • 黄色线的颜色是渐变的

导航条的整体构造

我们可以使用 HorizontalScrollView 来实现,HorizontalScrollView 只允许有一个子View,也就是图中的contextLi,而他的子View包括一个装有TextViewLinearLayout和一个左右跑动的DynamicLine,而在使用 TextView 填充 HorizontalScrollView 时,会出现两种情况:

  • 字数很短,没有超出屏幕
  • 字数很长,超出了屏幕宽度

得出以下:

  • 通过TextView的长度 + TextView的左右边距屏幕宽度比较,判断 TextView 的总长度与屏幕宽度的关系。
  • 导航条上面的分类字数较少时,要首先计算平分的每个 TextView 字体的宽度,然后指定 TextView 的左右边距。
  • 字数长时,设置 TextView 的左右边距为默认边距

根据 TextView 的实际长度计算其左右边距代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
private int getTextViewMargins(String[] titles) {
float countLength = 0;
TextView textView = new TextView(getContext());
textView.setTextSize(defaultTextSize);
TextPaint paint = textView.getPaint();

for (int i = 0; i < titles.length; i++) {
countLength = countLength + itemMargins + paint.measureText(titles[i]) + itemMargins;
}
int screenWidth = Tool.getScreenWidth(getContext());

if (countLength <= screenWidth) {
allTextViewLength = screenWidth;
return (screenWidth / titles.length - (int) paint.measureText(titles[0])) / 2;
} else {
allTextViewLength = (int) countLength;
return (int) itemMargins;
}
}

然后将相关的数据进行填充:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
private void createTextViews(String[] titles) {
LinearLayout contentLl = new LinearLayout(getContext());
contentLl.setLayoutParams(parentParams);
contentLl.setGravity(Gravity.CENTER_VERTICAL);
contentLl.setOrientation(LinearLayout.VERTICAL);
addView(contentLl);

LinearLayout textViewLl = new LinearLayout(getContext());
textViewLl.setLayoutParams(contentParams);

margin = getTextViewMargins(titles);

textViewParams.setMargins(margin, 0, margin, 0);

for (int i = 0; i < titles.length; i++) {
TextView textView = new TextView(getContext());
textView.setText(titles[i]);
textView.setTextColor(defaultTextColor);
textView.setTextSize(defaultTextSize);
textView.setLayoutParams(textViewParams);
textView.setGravity(Gravity.CENTER);
textView.setOnClickListener(onClickListener);
textView.setTag(i);
textViews.add(textView);
textViewLl.addView(textView);
}
contentLl.addView(textViewLl);
contentLl.addView(dynamicLine);
}

黄色的线-DynamicLine

可以看到黄色的线并不是一条线,而是一个圆角矩形。关于黄色圆角矩形的移动,只要更改圆角矩形的起始X坐标与终止X坐标,就可以让黄色条条进行移动了:主要用到了一个API:

1
drawRoundRect(@NonNull RectF rect, float rx, float ry, @NonNull Paint paint)

自定义一个 DynamicLine 继承 View,代码及说明如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
public class DynamicLine extends View {
private int lineHeight;
private int shaderColorEnd;
private int shaderColorStart;
private float startX, stopX;// 线的起点和终点
private Paint paint;
private RectF rectF = new RectF(startX, 0, stopX, 0);

public DynamicLine(Context context, int shaderColorStart, int shaderColorEnd, int lineHeight) {
this(context, null);
this.shaderColorStart = shaderColorStart;
this.shaderColorEnd = shaderColorEnd;
this.lineHeight = lineHeight;
init();
}

public DynamicLine(Context context, AttributeSet attrs) {
this(context, attrs, 0);
}

public DynamicLine(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
}

public void setShaderColorEnd(int shaderColorEnd) {
this.shaderColorEnd = shaderColorEnd;
}

public void setShaderColorStart(int shaderColorStart) {
this.shaderColorStart = shaderColorStart;
}

public void setLineHeight(int lineHeight) {
this.lineHeight = lineHeight;
}

public void init() {
paint = new Paint();
paint.setAntiAlias(true);// 消除锯齿
paint.setStyle(Paint.Style.FILL);
paint.setStrokeWidth(5);//画笔宽度
// 设置渐变颜色
paint.setShader(new LinearGradient(0, 100, Tool.getScreenWidth(getContext()), 100, shaderColorStart, shaderColorEnd, Shader.TileMode.MIRROR));
}

@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
// 自定义高度
heightMeasureSpec = MeasureSpec.makeMeasureSpec(lineHeight, MeasureSpec.getMode(heightMeasureSpec));
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
}

@Override
protected void onDraw(Canvas canvas) {
rectF.set(startX, 0, stopX, 10);
canvas.drawRoundRect(rectF, 5, 5, paint);// 圆角矩形的圆角曲率
}

// 根据起始、终点坐标更新黄色圆角,进行重绘
public void updateView(float startX, float stopX) {
this.startX = startX;
this.stopX = stopX;
invalidate();
}
}

我们知道当 viewpager 切换时动作与 DynamicLine 的 startX 与 stopX 的具体对应关系。可以使用 ViewPager的addOnPageChangeListener() 方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
public MyOnPageChangeListener(Context context, ViewPager viewPager, DynamicLine dynamicLine, ViewPagerTitle viewPagerTitle, int allLength, int margin) {
this.viewPagerTitle = viewPagerTitle;
this.pager = viewPager;
this.dynamicLine = dynamicLine;
textViews = viewPagerTitle.getTextView();
pagerCount = textViews.size();
screenWidth = getScreenWidth(context);

lineWidth = (int) getTextViewLength(textViews.get(0));

everyLength = allLength / pagerCount;
dis = margin;
}

上面的方法主要是初始化操作,主要的计算及状态设置在OnPageChangeListener监听中来操作:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
@Override
public void onPageScrolled(int position, float positionOffset, int positionOffsetPixels) {
if (lastPosition > position) {// 页面向右滑动
dynamicLine.updateView((position + positionOffset) * everyLength + dis, (lastPosition + 1) * everyLength - dis);
} else {// 页面向左滑动
if (positionOffset > 0.5f) {
positionOffset = 0.5f;
}
dynamicLine.updateView(lastPosition * everyLength + dis, (position + positionOffset * 2) * everyLength + dis + lineWidth);
}
}

@Override
public void onPageSelected(int position) {
Log.d(TAG, lastPosition + "[[[[");
viewPagerTitle.setCurrentItem(position);
}

@Override
public void onPageScrollStateChanged(int state) {
boolean scrollRight;//页面向右
if (state == SCROLL_STATE_SETTLING) {
scrollRight = lastPosition < pager.getCurrentItem();
lastPosition = pager.getCurrentItem();

if (lastPosition + 1 < textViews.size() && lastPosition - 1 >= 0) {
textViews.get(scrollRight ? lastPosition + 1 : lastPosition - 1).getLocationOnScreen(location);
if (location[0] > screenWidth) {
viewPagerTitle.smoothScrollBy(screenWidth / 2, 0);
} else if (location[0] < 0) {
viewPagerTitle.smoothScrollBy(-screenWidth / 2, 0);
}
}
}
}

与原文相比,我这里不再提供字体选中变大等效果,但是将结构进行了分解,方便拓展,毕竟每个人的需求不同,所以,这边不上传jcenter()地址,直接上 github地址
可以看到项目的目录结构:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
└── main
├── AndroidManifest.xml
├── java
│   └── sing
│   ├── flextitle
│   │   ├── DynamicLine.java
│   │   ├── Tool.java
│   │   └── ViewPagerTitle.java
│   └── viewpagerflextitle
│   ├── MainActivity.java
│   ├── MyOnPageChangeListener.java
│   └── MyPagerAdapter.java
└── res
├── layout
│   └── activity_main.xml
├── mipmap-xhdpi
│   └── ic_launcher.png
└── values
├── attr.xml
├── colors.xml
├── strings.xml
└── styles.xml

而我们只需拷贝java -> sing -> flextitle下的文件和res -> values -> attr.xml到项目中即可,剩下的部分为自定义,可参照demo中的代码进行自己修改。