- 独家发布于鸿洋公众号
大家好,我是徐爱卿。博客地址:flutterall.com
前言
老早就想写这篇博客了,demo早就完工了,博客到现在才写,惭愧。忘记什么时候开始看微博时,无意中注意到微博的导航条,好有趣,就无聊的拖过来拖过去。不多说,上图。
文章末尾有福利哦~~
可以看下微博,自己滑动试一试。
看到上面的黄色的条条,可长可短,邪恶~~
两个TAB页,关注和热门。
几个特点:
- 关注页面滑到页面的一半宽度以上时会自动切换到热门页面,这是ViewPager的特性。
- 关键看黄条的长度。当关注页面滑动一半时,黄条的长度 到达“热门”两个字的接近右边,不会边长。反之,亦然。
- 选中的页面的字体大小与颜色均有变化。
- 黄色线的颜色是渐变的(可以自己认真看下微博导航条的颜色)
看下我的实现:
开鲁
制作导航条的TextView
我们从上到下看看这个导航条是怎么制作的。对于这个,我们可以使用现成的HorizontalScrollView。也就是这个水平滑动的ScollView。使用TextView填充HorizontalScrollView时,会出现两种情况:
分析:
- 根据计算所有TextView的长度+TextView的左右边距与屏幕宽度比较,判断TextView的总长度大于小于屏幕宽度。
- 导航条上面的分类字数较少时,没有盛满,我们要首先计算平分的每个TextView字体的宽度,然后指定TextView的左右边距。
- 字数长时,我们设置TextView的左右边距为默认边距
根据TextView的实际长度计算其左右边距代码:
/**
*
* @param titleAry TextView的String字符串 “关注” “推荐”
* @return
*/
private int getTextViewMargins(String[] titleAry) {
int defaultMargins = 30;
float countLength = 0;
TextView textView = new TextView(getContext());
textView.setTextSize(defaultTextSize);
TextPaint paint = textView.getPaint();
for (int i = 0; i < titleAry.length; i++) {
countLength = countLength + defaultMargins + paint.measureText(titleAry[i]) + defaultMargins;
}
int screenWidth = getScreenWidth(getContext());
if (countLength <= screenWidth) { //TextView总长度小于屏幕宽度
allTextViewLength = screenWidth;
return (screenWidth / titleAry.length - (int) paint.measureText(titleAry[0])) / 2;
} else { //TextView总长度大于屏幕宽度
allTextViewLength = (int) countLength;
return defaultMargins;
}
}
知道了每个TextView的左右边距后(每个边距均一致,美观,并且绝大多数APP都是这样设计的,UED懂的),然后在一个个创建TextView添加到textViewLl中即可。
将所有TextView添加到contentLl中
ViewPagerTitle
/**
* Created by lovexujh on 2017/7/3
*/
public class ViewPagerTitle extends HorizontalScrollView {
private String[] titles;//导航条的字符串:关注、推荐 、视频。。。
private ArrayList<TextView> textViews = new ArrayList<>(); //导航条的所有TextView
private DynamicLine dynamicLine;
private ViewPager viewPager;
private MyOnPageChangeListener onPageChangeListener;//ViewPager的滑动监听
private int margin;//导航条的每两个TextView之间的间距
private LinearLayout.LayoutParams contentParams = new LinearLayout.LayoutParams(ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT);
private LinearLayout.LayoutParams textViewParams = new LinearLayout.LayoutParams(ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT);
private float defaultTextSize = 18;
private float selectedTextSize = 22;
private int defaultTextColor = Color.GRAY;
private int selectedTextColor = Color.BLACK;
private int allTextViewLength;
public ViewPagerTitle(Context context) {
this(context, null);
}
public ViewPagerTitle(Context context, AttributeSet attrs) {
this(context, attrs, 0);
}
public ViewPagerTitle(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
init();
}
private void init() {
}
public void initData(String[] titles, ViewPager viewPager, int defaultIndex) {
this.titles = titles;
this.viewPager = viewPager;
createDynamicLine();
createTextViews(titles);
int fixLeftDis = getFixLeftDis();
onPageChangeListener = new MyOnPageChangeListener(getContext(), viewPager, dynamicLine, this, allTextViewLength, margin, fixLeftDis);
setDefaultIndex(defaultIndex);
viewPager.addOnPageChangeListener(onPageChangeListener);
}
/**
* 这个方法是来修正TextView的左右边距的,
* 因为每个TextView而言 : leftMargins + TextViewLength + rightMargins 这三个的值要一致,
* 被选中的TExtView的TextViewLength要比默认没有选中的TextView的TextViewLength大,
* 所以选中的字体的左右边距要偏小。
* @return
*/
private int getFixLeftDis() {
TextView textView = new TextView(getContext());
textView.setTextSize(defaultTextSize);
textView.setText(titles[0]);
float defaultTextSize = getTextViewLength(textView);
textView.setTextSize(selectedTextSize);
float selectTextSize = getTextViewLength(textView);
return (int)(selectTextSize - defaultTextSize) / 2;
}
public ArrayList<TextView> getTextView() {
return textViews;
}
private void createDynamicLine() {
ViewGroup.LayoutParams params = new ViewGroup.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT);
dynamicLine = new DynamicLine(getContext());
dynamicLine.setLayoutParams(params);
}
private void createTextViews(String[] titles) {
LinearLayout contentLl = new LinearLayout(getContext());
contentLl.setBackgroundColor(Color.parseColor("#fffacd"));
contentLl.setLayoutParams(contentParams);
contentLl.setOrientation(LinearLayout.VERTICAL);
addView(contentLl);
LinearLayout textViewLl = new LinearLayout(getContext());
textViewLl.setLayoutParams(contentParams);
textViewLl.setOrientation(LinearLayout.HORIZONTAL);
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(Color.GRAY);
textView.setTextSize(defaultTextSize);
textView.setLayoutParams(textViewParams);
textView.setGravity(Gravity.CENTER_HORIZONTAL);
textView.setOnClickListener(onClickListener);
textView.setTag(i);
textViews.add(textView);
textViewLl.addView(textView);
}
contentLl.addView(textViewLl); //将所有的TextView所在的LinerLayout添加到HorizontalScrollView的contentLl中
contentLl.addView(dynamicLine);//dynamicLine是左右跑动的黄色的线
}
/**
*
* @param titleAry TextView的String字符串 “关注” “推荐”
* @return
*/
private int getTextViewMargins(String[] titleAry) {
int defaultMargins = 30;
float countLength = 0;
TextView textView = new TextView(getContext());
textView.setTextSize(defaultTextSize);
TextPaint paint = textView.getPaint();
for (int i = 0; i < titleAry.length; i++) {
countLength = countLength + defaultMargins + paint.measureText(titleAry[i]) + defaultMargins;
}
int screenWidth = getScreenWidth(getContext());
if (countLength <= screenWidth) { //TextView总长度小于屏幕宽度
allTextViewLength = screenWidth;
return (screenWidth / titleAry.length - (int) paint.measureText(titleAry[0])) / 2;
} else { //TextView总长度大于屏幕宽度
allTextViewLength = (int) countLength;
return defaultMargins;
}
}
private OnClickListener onClickListener = new OnClickListener() {
@Override
public void onClick(View v) {
setCurrentItem((int) v.getTag());
viewPager.setCurrentItem((int) v.getTag());
}
};
public void setDefaultIndex(int index) {
setCurrentItem(index);
}
public void setCurrentItem(int index) {
for (int i = 0; i < textViews.size(); i++) {
if (i == index) {
textViews.get(i).setTextColor(selectedTextColor);
textViews.get(i).setTextSize(selectedTextSize);
} else {
textViews.get(i).setTextColor(defaultTextColor);
textViews.get(i).setTextSize(defaultTextSize);
}
}
}
@Override
protected void onDetachedFromWindow() {
super.onDetachedFromWindow();
viewPager.removeOnPageChangeListener(onPageChangeListener);
}
}
黄色的线-DynamicLine
可以看到黄色的线并不是一条线,而是一个圆角矩形。这就可以使用drawRoundRect(@NonNull RectF rect, float rx, float ry, @NonNull Paint paint) 这个API。
关键点在于,黄色圆角矩形的移动,只要更改圆角矩形的起始X坐标与终止X坐标。这样就可以让黄色条条进行移动了
来自定义一个DynamicLine继承View,代码及说明如下:
public class DynamicLine extends View {
private float startX, stopX;//的起始X,终止X坐标。
private Paint paint;
private RectF rectF = new RectF(startX, 0, stopX, 0);//RectF指的是float精度的矩形
public DynamicLine(Context context) {
this(context, null);
}
public DynamicLine(Context context, AttributeSet attrs) {
this(context, attrs, 0);
}
public DynamicLine(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
init();
}
private void init() {
paint = new Paint();
paint.setAntiAlias(true);//抗锯齿
paint.setStyle(Paint.Style.FILL);//填充
paint.setStrokeWidth(5);//画笔宽度
paint.setShader(new LinearGradient(0, 100, getScreenWidth(getContext()), 100, Color.parseColor("#ffc125"), Color.parseColor("#ff4500"), Shader.TileMode.MIRROR));//设置画笔渐变色
}
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {//自定义DynamicLine的高度
heightMeasureSpec = MeasureSpec.makeMeasureSpec(20, 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);//圆角矩形的圆角的曲率
}
/**
* 根据起始、终止坐标更新黄色圆角,进行重新绘制
* @param startX
* @param stopX
*/
public void updateView(float startX, float stopX) {//
this.startX = startX;
this.stopX = stopX;
invalidate();
}
}
我们把DynamicLine放到activity中添加下面代码,测试一下,效果:
public class MainActivity extends AppCompatActivity {
private DynamicLine dynamicLine;
private float startX, stopX;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
dynamicLine = (DynamicLine)findViewById(R.id.dynamicLine);
// init();
}
@Override
public boolean dispatchTouchEvent(MotionEvent ev) {
switch (ev.getAction()){
case MotionEvent.ACTION_DOWN:
startX = ev.getRawX();
case MotionEvent.ACTION_MOVE:
stopX = ev.getRawX();
dynamicLine.updateView(startX, stopX);
}
return super.dispatchTouchEvent(ev);
}
}
后面我们需要知道当viewpager切换时动作与DynamicLine的startX与stopX的具体对应关系。可以使用ViewPager的addOnPageChangeListener(OnPageChangeListener listener)方法。
OnPageChangeListener 的实现
public class MyOnPageChangeListener implements ViewPager.OnPageChangeListener {
private int fixLeftDis;
private ArrayList<TextView> textViews;
private ViewPagerTitle viewPagerTitle;
private DynamicLine dynamicLine;
private ViewPager pager;
private int pagerCount;
private int screenWidth;
private int lineWidth;
private int everyLength;
private int lastPosition;
private int dis;
private int[] location = new int[2];
/**
*
* @param context
* @param viewPager
* @param dynamicLine
* @param viewPagerTitle
* @param allLength 所有的TextView的总长度。
* @param margin TextView的左右边距。
* @param fixLeftDis TextView的修正的距离
*/
public MyOnPageChangeListener(Context context, ViewPager viewPager, DynamicLine dynamicLine, ViewPagerTitle viewPagerTitle, int allLength, int margin, int fixLeftDis) {
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;
this.fixLeftDis = fixLeftDis;
}
/**
*
* @param position
* @param positionOffset 当前页面的便宜百分小数 [0, 1)
* @param positionOffsetPixels 当前页面的偏移像素 0 ~ 屏幕宽度
*/
@Override
public void onPageScrolled(int position, float positionOffset, int positionOffsetPixels) {
if (lastPosition > position) {//页面向右滚动
/**
* 档页面向右滚动时,dynamicLine的右边的stopX位置不变,startX在变化。
*/
dynamicLine.updateView((position + positionOffset) * everyLength + dis + fixLeftDis, (lastPosition + 1) * everyLength - dis);
} else { //页面向左滚动
/**
* 档页面向左滚动时,dynamicLine的左边的startX位置不变,stopX在变化。
*/
if (positionOffset > 0.5f) {
positionOffset = 0.5f;
}
dynamicLine.updateView(lastPosition * everyLength + dis + fixLeftDis, (position + positionOffset * 2) * everyLength + dis + lineWidth);
}
}
@Override
public void onPageSelected(int position) {
viewPagerTitle.setCurrentItem(position);
}
/**
* state 的几个状态:
* SCROLL_STATE_IDLE 挂起,空闲,页面处于静止状态
* SCROLL_STATE_DRAGGING 拖拽,页面处于拖拽状态
* SCROLL_STATE_SETTLING 设置,手指滑动后当手指离开页面时
* @param state
*/
@Override
public void onPageScrollStateChanged(int state) {
boolean scrollRight;//页面向右
if (state == SCROLL_STATE_SETTLING) {
scrollRight = lastPosition < pager.getCurrentItem();
lastPosition = pager.getCurrentItem();
/**
* 下面几行代码,解决页面滑到的TAB页时对应的TextView对应,TextView处于屏幕外面,
* 这个时候就需要将HorizontalScrollView滑动到屏幕中间。
*/
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);
}
}
}
}
}
Tool 工具类
/**
* Created by lovexujh on 2017/7/4
*/
public class Tool {
public static float getTextViewLength(TextView textView) {
TextPaint paint = textView.getPaint();
return paint.measureText(textView.getText().toString());
}
public static float getTextViewLength(TextView textView, float textSize) {
TextPaint paint = textView.getPaint();
paint.setTextSize(textSize);
return paint.measureText(textView.getText().toString());
}
public static int getScreenWidth(Context context) {
WindowManager wm = (WindowManager) context.getSystemService(Context.WINDOW_SERVICE);
DisplayMetrics dm = new DisplayMetrics();
wm.getDefaultDisplay().getMetrics(dm);
return dm.widthPixels;
}
}
最后
其实,整个文章的难点在于如何设计DynamicLine,刚开始想着很简单 ,但是真到你自己去写写,很多问题。比如,如何确定滑动时的DynamicLine位置,以及当一个TextView被选中时,它的字体宽度是变大了,这个时候DynamicLine的起末位置怎么办 等等。不信,大神你撸一把试试。
为了方便使用 ,对上面的代码优化了,自定义了属性,上到了GitHub,可以查看最新Dev分支。截止发稿时,为dev1.0.1 。欢迎大家多多fork多多start,O(∩_∩)O多谢!
更多详细使用方式见下面👇!
地址:https://github.com/kaina404/ViewPagerFlexTitle/tree/dev-1.0.1
看着下面这个APP火了,闲着没事,抓包自己搞了一个,也算是高仿了巴。
福利,欢迎大家多多fork多多start,O(∩_∩)O谢谢。