仿微博导航条

作者: 徐爱卿 | 来源:发表于2017-07-12 23:50 被阅读1920次
前进

前言

老早就想写这篇博客了,demo早就完工了,博客到现在才写,惭愧。忘记什么时候开始看微博时,无意中注意到微博的导航条,好有趣,就无聊的拖过来拖过去。不多说,上图。
文章末尾有福利哦~~

微博导航条

可以看下微博,自己滑动试一试。

看到上面的黄色的条条,可长可短,邪恶~~

两个TAB页,关注和热门。
几个特点:

  • 关注页面滑到页面的一半宽度以上时会自动切换到热门页面,这是ViewPager的特性。
  • 关键看黄条的长度。当关注页面滑动一半时,黄条的长度 到达“热门”两个字的接近右边,不会边长。反之,亦然。
  • 选中的页面的字体大小与颜色均有变化。
  • 黄色线的颜色是渐变的(可以自己认真看下微博导航条的颜色)
开车了

看下我的实现:

基础版 升级版

开鲁

导航条的整体构造

制作导航条的TextView

导航条的滑动

我们从上到下看看这个导航条是怎么制作的。对于这个,我们可以使用现成的HorizontalScrollView。也就是这个水平滑动的ScollView。使用TextView填充HorizontalScrollView时,会出现两种情况:

HorizontalScrollView与TextView

分析:

  • 根据计算所有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 

```java
/**
 * 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中添加下面代码,测试一下,效果:

```java
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);
    }
}```

 
![DynamicLine](https://img.haomeiwen.com/i3884536/b7cf8002944cadfb.gif?imageMogr2/auto-orient/strip)

![有渐变色,有效果。可以,没问题。](https://img.haomeiwen.com/i3884536/3f197a576d38f0ef.jpg?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240)
> 后面我们需要知道当viewpager切换时动作与DynamicLine的startX与stopX的具体对应关系。可以使用ViewPager的addOnPageChangeListener(OnPageChangeListener listener)方法。

**OnPageChangeListener 的实现**
```java
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 工具类
```java
/**
 * 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/xujianhui404/ViewPagerFlexTitle/tree/dev-1.0.1

看着下面这个APP火了,闲着没事,抓包自己搞了一个,也算是高仿了巴。

福利,欢迎大家多多fork多多start,O(∩_∩)O谢谢。

相关文章

网友评论

  • _xiangpan:根据大神的dome做了些修改https://www.jianshu.com/p/463df45913f8
  • b8d0f44e9712:请问怎么控制横线的长度,我引用后线的长度和文本的长度总是一样的
  • 似水流年_1af4:大神,你有没有解决"二二"."三三三"."四四四四" 不居中的问题啊?就是3楼提出的那个问题啊?现在急需啊....
    Jey欧巴:我弄出来了,花了3个小时计算TextView坐标位置和动画,根据onPageScrolled里面的偏移量计算滑动过程中TextView的坐标变化,获取TextView长度的时候要动态获取
  • 小白_Sing:在 ViewPagerTitle 中createTextViews方法中:
    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.setTextColor(Color.GRAY);这句应该改为textView.setTextColor(defaultTextColor);
    否则设置默认字体颜色在不滑动的情况下是无效的
  • 小白_Sing:同时在ViewPagerTitle中有个方法可以优化:
    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);
      }
     }
    }
    改为:
    public void setCurrentItem(int index) {
     for (int i = 0; i < textViews.size(); i++) {
      textViews.get(i).setTextColor(defaultTextColor);
      textViews.get(i).setTextSize(defaultTextSize);
     }

     textViews.get(index).setTextColor(selectedTextColor);
     textViews.get(index).setTextSize(selectedTextSize);
    }
    这样就不用每次循环都去判断了,还有部分代码未看完
  • 小白_Sing:我觉得你这个结构写的不利于扩展,我重写了结构,http://upload-images.jianshu.io/upload_images/7115680-4758426f8de65ae7.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240,这样可以自己定义OnPageChangeListener等
  • developerYk:徐哥 如果当标题只有2组的时候 怎么让它显示跟微博一样居中啊?
    HuiRan:楼主,当标题为四个,三个字的时候,DynamicLine 这个线不居中,这个问题解决了吗?
    我建议在加个功能,DynamicLine可以和标题的长度一样长:smiley:
    developerYk:@徐爱卿 嗯,好的 谢谢!
    徐爱卿:@toStringy 可以试着重写下这个控件啊 相信自己可以的
  • 张瑾瑜:楼主请问这个库源码在哪里找到啊?
  • e6850ef60d12:楼主,这个问题 是不是还没有上传,今天下载了demo是老版本的,项目用到了,谢谢!
    当标题为四个四,三个字的时候,你会发现DynamicLine 这个线不居中。
    eg:viewPagerTitle.initData(new String[]{"关注", "推荐内容", "视频", "直播间", "图片", "段子", "精华", "热门"}, pager, 0);
    求大神给出解决方案。
  • 深爱蒲公英的纯美:我下载下来的是demo的源码,库源码怎么下载
    深爱蒲公英的纯美: @张瑾瑜 根据名字直接搜,
    张瑾瑜:这个库源码在哪里啊?
    深爱蒲公英的纯美:不好意思找到了
  • ac24966c105b:大神,你抓包的那个项目,app运行里面什么内容都没有,求分享,安卓抓包APP教程。
    徐爱卿:@岁月清浅最安然 我反编译过他们第一版本的代码 怎么说呢 那写的叫做:sweat:
    徐爱卿:@岁月清浅最安然 我刚才看了下 估计是他们PM强制用户升级了
    徐爱卿:@岁月清浅最安然 赋予网络权限 直接安装git上面的apk 应该没问题
  • 15d7c82e493f:楼主你好貌似,现在设置默认页面还有一些下bug,我现在有3张page,我设置的默认page是第三张,也就position=2,然后现在这个dynamicLine 会变成从第一张对应的StartX位置到第三张位置到StopX位置的长度的线。大概就是这样,希望您指点一二:smile:
    徐爱卿:@15d7c82e493f 在listener初始化的时候设置lastPosition的值
    徐爱卿:@15d7c82e493f 你把defaultIndex传到onPagerListener中的lastPosition试一下
    徐爱卿:@15d7c82e493f 好的 我了解了 你也看看原因 我最近比较忙 有时间看下 如果实在解不开再问下我 谢谢
  • 7797afe690aa:大神,我用了你的这个控件,感觉很不错,可是现在出现一个bug,希望你能帮帮我,我们项目下周就要上线,谢谢!!!
    bug是:我在外层又一个fragment,这个fragment中间又有几个fragment,然后内层这个fragment是用你的这个控件关联的.
    现在我外层fragment一旦切换过,再切换回来的时候,这个控件下面的滑动条就不动了,这个怎么办?谢谢
    徐爱卿:@楠108 由于对你的代码逻辑不清楚,所以不敢妄下定论。你可以debug看看dynamicLine的onDraw方法是否被调用咯。然后在看看是否调用了dynamicLine的update方法,以及pagerlistener中的监听。个人感觉可能是触摸时间监听冲突了,或者被拦截了。
  • 庞哈哈哈12138:厉害厉害 ,给你我的小心心
  • 北极星_YJH:您好,在多个table时可滑动,我想让table选中时居左,我在您的代码上改了白天,并没有实现。希望作者能看到,能够帮帮我。
    北极星_YJH:@徐爱卿 我在MyOnPageChangeListener的onPageScrollStateChanged方法中更改了许久,就是没有调好位置,希望作者能帮帮忙:pray:
    北极星_YJH:@徐爱卿 点击相应tab如“推荐”则展示相应栏目内容,且“推荐”自动滑动到最左侧原“关注”的位置
    徐爱卿:@PinkCandy 具体说下 我不太明白
  • 零起点行天下:当标题为四个四,三个字的时候,你会发现DynamicLine 这个线不居中。
    eg:viewPagerTitle.initData(new String[]{"关注", "推荐内容", "视频", "直播间", "图片", "段子", "精华", "热门"}, pager, 0);
    求大神给出解决方案。
    徐爱卿:现在赶项目 忙 周末再解决 谢谢
    徐爱卿:我看下如何解决
    徐爱卿:是的 有这个问题
  • 952ae17f7ee0:期待已久!

本文标题:仿微博导航条

本文链接:https://www.haomeiwen.com/subject/apiuhxtx.html