仿微博导航条

  • 独家发布于鸿洋公众号

大家好,我是徐爱卿。博客地址:flutterall.com

前进

前言

老早就想写这篇博客了,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
/**
 * 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);
    }
}
DynamicLine
有渐变色,有效果。可以,没问题。

后面我们需要知道当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谢谢。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 218,546评论 6 507
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 93,224评论 3 395
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 164,911评论 0 354
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 58,737评论 1 294
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 67,753评论 6 392
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,598评论 1 305
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,338评论 3 418
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 39,249评论 0 276
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,696评论 1 314
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,888评论 3 336
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 40,013评论 1 348
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,731评论 5 346
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 41,348评论 3 330
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,929评论 0 22
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 33,048评论 1 270
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 48,203评论 3 370
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,960评论 2 355

推荐阅读更多精彩内容