最近在做一个项目的时候,需要实现像今天头条那样的顶部导航栏效果,通过在网上了解自定义View的相关知识和看别人的博客,最终实现,本文既作为一个记录,同时也给需要的人提供参考

    主要的思想是CategoryTabStrip 类作为一个容器,包含ColorTrackView,和ViewPager联动逻辑在CategoryTabStrip类里实现,字体变色在ColorTrackView里实现。

    首先要实现顶部导航栏的滑动效果,我们可以自定义一个View继承自HorizontalScrollView

public class CategoryTabStrip extends HorizontalScrollView {
    private LayoutInflater mLayoutInflater;
//    private final PageListener pageListener = new PageListener();
    private ViewPager pager;
    private LinearLayout tabsContainer;
    private int tabCount;

    private int currentPosition = 0;
    private float currentPositionOffset = 0f;

    private Rect indicatorRect;

    private LinearLayout.LayoutParams defaultTabLayoutParams;

    private int scrollOffset = 10;
    private int lastScrollX = 0;

    private Context mContext;


    public CategoryTabStrip(Context context) {
        this(context, null);
        this.mContext = context;
    }

    public CategoryTabStrip(Context context, AttributeSet attrs) {
        this(context, attrs, 0);
        this.mContext = context;
    }

    public CategoryTabStrip(Context context, AttributeSet attrs, int defStyle) {
        super(context, attrs, defStyle);
        this.mContext = context;
        mLayoutInflater = LayoutInflater.from(context);

        setFillViewport(true);
        setWillNotDraw(false);
        indicatorRect = new Rect();

        // 标签容器
        tabsContainer = new LinearLayout(context);
        tabsContainer.setOrientation(LinearLayout.HORIZONTAL);
        tabsContainer.setLayoutParams(new LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT));
        addView(tabsContainer);

        DisplayMetrics dm = getResources().getDisplayMetrics();
        scrollOffset = (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, scrollOffset, dm);

        defaultTabLayoutParams = new LinearLayout.LayoutParams(LayoutParams.WRAP_CONTENT, LayoutParams.MATCH_PARENT);

    }

    // 绑定与CategoryTabStrip控件对应的ViewPager控件,实现联动
    public void setViewPager(ViewPager pager) {
        this.pager = pager;
        if (pager.getAdapter() == null) {
            throw new IllegalStateException("ViewPager does not have adapter instance.");
        }
        initEvents();
        notifyDataSetChanged();
    }

    // 当附加在ViewPager适配器上的数据发生变化时,应该调用该方法通知CategoryTabStrip刷新数据
    public void notifyDataSetChanged() {
        tabsContainer.removeAllViews();
        mTabs.clear();
        tabCount = pager.getAdapter().getCount();
        for (int i = 0; i < tabCount; i++) {
            addTab(i, pager.getAdapter().getPageTitle(i).toString());
        }
    }

    // 添加一个标签到导航菜单
    public void addTab(final int position, String title) {
        ViewGroup tabLayer = (ViewGroup) mLayoutInflater.inflate(R.layout.category_tab, this, false);
        final ColorTrackView trackView = tabLayer.findViewById(R.id.tabTrackView);
        trackView.setText(title);
        if(position == 0){
            trackView.setProgress(1);
            RelativeLayout.LayoutParams lp = new RelativeLayout.LayoutParams(RelativeLayout.LayoutParams.WRAP_CONTENT,
                    RelativeLayout.LayoutParams.WRAP_CONTENT);
            lp.setMargins(V.dp2px(15),0,0,0);
            lp.addRule(RelativeLayout.CENTER_VERTICAL);
            trackView.setLayoutParams(lp);
        }else{
            RelativeLayout.LayoutParams lp = new RelativeLayout.LayoutParams(RelativeLayout.LayoutParams.WRAP_CONTENT,
                    RelativeLayout.LayoutParams.WRAP_CONTENT);
            lp.setMargins(V.dp2px(20),0,0,0);
            lp.addRule(RelativeLayout.CENTER_VERTICAL);
            trackView.setLayoutParams(lp);
        }
        trackView.setOnClickListener(new OnClickListener() {
            @Override
            public void onClick(View v) {
                int index = getTabIndex(trackView.getText());
                if(index != -1) {
                    setOneTabLight(index);
                    Toast.makeText(getContext(), "选中啦 " + trackView.getText(), Toast.LENGTH_SHORT).show();
                    pager.setCurrentItem(index, false);
                }else{
                    Toast.makeText(getContext(),"out of range -> index: -1", Toast.LENGTH_SHORT).show();
                }
            }
        });
        Log.e("CategoryTabStrip","addview");
        mTabs.add(trackView);
        tabsContainer.addView(tabLayer, position);
    }

    private int getTabIndex(String title){
        for (int i = 0; i < tabCount; i++) {
            if(title.equals(pager.getAdapter().getPageTitle(i).toString())){
                return i;
            }
        }
        return -1;
    }

    //设置指定位置tab高亮,其他tab原始颜色
    private void setOneTabLight(int position){
        for(int i = 0; i < mTabs.size(); i++){
            if(i == position){
                mTabs.get(i).setProgress(1);
            }
            else{
                mTabs.get(i).setProgress(0);
            }
        }
    }

    private List<ColorTrackView> mTabs = new ArrayList<ColorTrackView>();

    // 计算滚动范围
    private int getScrollRange() {
        return getChildCount() > 0 ? Math.max(0, getChildAt(0).getWidth() - getWidth() + getPaddingLeft() + getPaddingRight()) : 0;
    }

    private void initEvents() {
        pager.setOnPageChangeListener(new OnPageChangeListener() {
            @Override
            public void onPageSelected(int position)
            {

                Log.e("CategoryTabStrip","pageSelected...." + position);
            }

            @Override
            public void onPageScrolled(int position, float positionOffset,
                                       int positionOffsetPixels) {
                Log.e("CategoryTabStrip","pageScrolled.");
                currentPosition = position;
                currentPositionOffset = positionOffset;
                scrollToChild(position, (int) (positionOffset * tabsContainer.getChildAt(position).getWidth()),
                        positionOffset);

//                invalidate();

            }

            @Override
            public void onPageScrollStateChanged(int state)
            {
                if (state == ViewPager.SCROLL_STATE_IDLE) {
                    if(pager.getCurrentItem() == 0) {
                        // 滑动到最左边
                        scrollTo(0, 0);
                    } else if (pager.getCurrentItem() == tabCount - 1) {
                        // 滑动到最右边
                        scrollTo(getScrollRange(), 0);
                    } else {
                        scrollToChild(pager.getCurrentItem(),0, 0);
                    }
                }
            }
        });
    }

    // 计算滑动过程中矩形高亮区域的上下左右位置
    private void calculateIndicatorRect(Rect rect) {
        ViewGroup currentTab = (ViewGroup)tabsContainer.getChildAt(currentPosition);
//        TextView category_text = (TextView) currentTab.findViewById(R.id.category_text);
        ColorTrackView colorTrackView = (ColorTrackView) currentTab.findViewById(R.id.tabTrackView);

        float left = (float) (currentTab.getLeft() + colorTrackView.getLeft());
        float width = ((float) colorTrackView.getWidth()) + left;

        if (currentPositionOffset > 0f && currentPosition < tabCount - 1) {
            ViewGroup nextTab = (ViewGroup)tabsContainer.getChildAt(currentPosition + 1);
            ColorTrackView next_category_text = (ColorTrackView) nextTab.findViewById(R.id.tabTrackView);

            float next_left = (float) (nextTab.getLeft() + next_category_text.getLeft());
            left = left * (1.0f - currentPositionOffset) + next_left * currentPositionOffset;
            width = width * (1.0f - currentPositionOffset) + currentPositionOffset * (((float) next_category_text.getWidth()) + next_left);
        }
        rect.set(((int) left) + getPaddingLeft(), getPaddingTop() + currentTab.getTop() + colorTrackView.getTop(),
                ((int) width) + getPaddingLeft(), currentTab.getTop() + getPaddingTop() + colorTrackView.getTop() + colorTrackView.getHeight());
    }

    // CategoryTabStrip与ViewPager联动逻辑
    private void scrollToChild(int position, int offset, float positionOffset) {
        if (tabCount == 0) {
            return;
        }

        calculateIndicatorRect(indicatorRect);
        int newScrollX = lastScrollX;
        if (indicatorRect.left < getScrollX() + scrollOffset) {
            newScrollX = indicatorRect.left - scrollOffset;
        } else if (indicatorRect.right > getScrollX() + getWidth() - scrollOffset) {
            newScrollX = indicatorRect.right - getWidth() + scrollOffset;
        }
        if (newScrollX != lastScrollX) {
            lastScrollX = newScrollX;
            scrollTo(newScrollX, 0);
        }

        if (positionOffset > 0) {
            ColorTrackView left = mTabs.get(position);
            ColorTrackView right = mTabs.get(position + 1);

            left.setDirection(1);
            right.setDirection(0);
            Log.e("TAG", positionOffset+"");
            left.setProgress( 1-positionOffset);
            right.setProgress(positionOffset);
        }
    }
}

    item点击事件处理:

trackView.setOnClickListener(new OnClickListener() {
            @Override
            public void onClick(View v) {
                int index = getTabIndex(trackView.getText());
                if(index != -1) {
                    setOneTabLight(index);
                    Toast.makeText(getContext(), "选中啦 " + trackView.getText(), Toast.LENGTH_SHORT).show();
                    pager.setCurrentItem(index, false);
                }else{
                    Toast.makeText(getContext(),"out of range -> index: -1", Toast.LENGTH_SHORT).show();
                }
            }
        });

    主要看pager.setCurrentItem(index, false)这一句代码,setCurrentItem函数中的第二个参数设置为false,在进行点击item进行pager选择的时候不进行动画滑动,避免了多次回调onPageScrolled方法,导致字体变色错乱的问题

    下面是布局文件fragment.xml:

<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:orientation="vertical"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:background="@color/white">

    <RelativeLayout
        android:id="@+id/category_layout"
        android:layout_width="match_parent"
        android:layout_height="48dp">
        <LinearLayout android:layout_width="match_parent"
            android:layout_height="40dp"
            android:layout_alignParentStart="true"
            android:layout_centerVertical="true"
            android:layout_marginRight="45dp">

            <com.wk.schoollife.widget.CategoryTabStrip
                android:id="@+id/category_strip"
                android:paddingLeft="6.0dip"
                android:paddingRight="6.0dip"
                android:clipToPadding="false"
                android:layout_width="wrap_content"
                android:layout_height="48dp" />

        </LinearLayout>

        <RelativeLayout
            android:layout_width="wrap_content"
            android:layout_height="match_parent"
            android:layout_alignParentEnd="true"
            android:background="@color/white">
            <ImageView
                android:id="@+id/column_to_right"
                android:layout_width="20dp"
                android:layout_height="20dp"
                android:layout_centerVertical="true"
                android:layout_marginRight="15dp"
                android:layout_marginLeft="10dp"
                android:src="@drawable/more1" />

            <View
                android:layout_width="1px"
                android:layout_height="20dp"
                android:layout_centerVertical="true"
                android:layout_alignParentStart="true"
                android:background="@color/found_border"
                />

        </RelativeLayout>




    </RelativeLayout>

    <View
        android:id="@+id/inteval"
        android:layout_below="@+id/category_layout"
        android:layout_width="match_parent"
        android:layout_height="8dp"
        android:background="@color/grayBg"/>

    <android.support.v4.view.ViewPager
        android:id="@+id/view_pager"
        android:layout_width="fill_parent"
        android:layout_height="fill_parent"
        android:layout_below="@+id/inteval">

    </android.support.v4.view.ViewPager>


</RelativeLayout>

包含ColorTrackView的布局文件:

category_tal.xml:

<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:zhy="http://schemas.android.com/apk/res-auto"
    android:orientation="vertical"
    android:layout_width="wrap_content"
    android:layout_height="match_parent">

    <com.wk.schoollife.widget.ColorTrackView
        android:id="@+id/tabTrackView"
        zhy:progress="0"
        zhy:text="简介"
        zhy:text_change_color="#ffff0000"
        zhy:text_origin_color="#ff000000"
        zhy:text_size="18sp"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_centerVertical="true" />

</RelativeLayout>

最后贴下ColorTrackView的代码:

/**
 *
 * @author zhy
 *
 */
public class ColorTrackView extends View {

    private int mTextStartX;
    private int mTextStartY;

//    public enum Direction {
//        LEFT, RIGHT, TOP, BOTTOM;
//    }

    private int mDirection = DIRECTION_LEFT;

    private static final int DIRECTION_LEFT = 0;
    private static final int DIRECTION_RIGHT = 1;
    private static final int DIRECTION_TOP = 2;
    private static final int DIRECTION_BOTTOM = 3;

    public void setDirection(int direction) {
        mDirection = direction;
    }

    private String mText = "张鸿洋";
    private Paint mPaint;
    private int mTextSize = sp2px(30);

    private int mTextOriginColor = 0xff333333;
    private int mTextChangeColor = 0xffff0000;

    private Rect mTextBound = new Rect();
    private int mTextWidth;
    private int mTextHeight;

    private float mProgress;

    public ColorTrackView(Context context) {
        super(context, null);
        mPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
        mPaint.setTextSize(mTextSize);
    }

    public ColorTrackView(Context context, AttributeSet attrs) {
        super(context, attrs);

        mPaint = new Paint(Paint.ANTI_ALIAS_FLAG);

        TypedArray ta = context.obtainStyledAttributes(attrs,
                R.styleable.ColorTrackView);
        mText = ta.getString(R.styleable.ColorTrackView_text);
        mTextSize = ta.getDimensionPixelSize(
                R.styleable.ColorTrackView_text_size, mTextSize);
        mTextOriginColor = ta.getColor(
                R.styleable.ColorTrackView_text_origin_color, mTextOriginColor);
        mTextChangeColor = ta.getColor(
                R.styleable.ColorTrackView_text_change_color, mTextChangeColor);
        mProgress = ta.getFloat(R.styleable.ColorTrackView_progress, 0);

        mDirection = ta
                .getInt(R.styleable.ColorTrackView_direction, mDirection);

        ta.recycle();

        mPaint.setTextSize(mTextSize);

    }

    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {

        measureText();

        int width = measureWidth(widthMeasureSpec);
        int height = measureHeight(heightMeasureSpec);
        setMeasuredDimension(width, height);

        mTextStartX = getMeasuredWidth() / 2 - mTextWidth / 2;
        mTextStartY = getMeasuredHeight() / 2 - mTextHeight / 2;
    }

    private void measureText() {
        mTextWidth = (int) mPaint.measureText(mText);
        FontMetrics fm = mPaint.getFontMetrics();
        mTextHeight = (int) Math.ceil(fm.descent - fm.top);

        mPaint.getTextBounds(mText, 0, mText.length(), mTextBound);
        mTextHeight = mTextBound.height();
    }

    private int measureHeight(int measureSpec) {
        int mode = MeasureSpec.getMode(measureSpec);
        int val = MeasureSpec.getSize(measureSpec);
        int result = 0;
        switch (mode) {
            case MeasureSpec.EXACTLY:
                result = val;
                break;
            case MeasureSpec.AT_MOST:
            case MeasureSpec.UNSPECIFIED:
                result = mTextBound.height();
                result += getPaddingTop() + getPaddingBottom();
                break;
        }
        result = mode == MeasureSpec.AT_MOST ? Math.min(result, val) : result;
        return result;
    }

    private int measureWidth(int measureSpec) {
        int mode = MeasureSpec.getMode(measureSpec);
        int val = MeasureSpec.getSize(measureSpec);
        int result = 0;
        switch (mode) {
            case MeasureSpec.EXACTLY:
                result = val;
                break;
            case MeasureSpec.AT_MOST:
            case MeasureSpec.UNSPECIFIED:
                // result = mTextBound.width();
                result = mTextWidth;
                result += getPaddingLeft() + getPaddingRight();
                break;
        }
        result = mode == MeasureSpec.AT_MOST ? Math.min(result, val) : result;
        return result;
    }


    public void reverseColor() {
        int tmp = mTextOriginColor;
        mTextOriginColor = mTextChangeColor;
        mTextChangeColor = tmp;
    }

    @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);

        int r = (int) (mProgress * mTextWidth + mTextStartX);
        int t = (int) (mProgress * mTextHeight + mTextStartY);

        if (mDirection == DIRECTION_LEFT) {
            drawChangeLeft(canvas, r);
            drawOriginLeft(canvas, r);
        } else if (mDirection == DIRECTION_RIGHT) {
            drawOriginRight(canvas, r);
            drawChangeRight(canvas, r);
        } else if (mDirection == DIRECTION_TOP) {
            drawOriginTop(canvas, t);
            drawChangeTop(canvas, t);
        } else {
            drawOriginBottom(canvas, t);
            drawChangeBottom(canvas, t);
        }

    }

    private boolean debug = false;

    private void drawText_h(Canvas canvas, int color, int startX, int endX) {
        mPaint.setColor(color);
        if (debug) {
            mPaint.setStyle(Style.STROKE);
            canvas.drawRect(startX, 0, endX, getMeasuredHeight(), mPaint);
        }
        canvas.save(Canvas.CLIP_SAVE_FLAG);
        canvas.clipRect(startX, 0, endX, getMeasuredHeight());// left, top,
        // right, bottom
        canvas.drawText(mText, mTextStartX,
                getMeasuredHeight() / 2
                        - ((mPaint.descent() + mPaint.ascent()) / 2), mPaint);
        canvas.restore();
    }

    private void drawText_v(Canvas canvas, int color, int startY, int endY) {
        mPaint.setColor(color);
        if (debug) {
            mPaint.setStyle(Style.STROKE);
            canvas.drawRect(0, startY, getMeasuredWidth(), endY, mPaint);
        }

        canvas.save(Canvas.CLIP_SAVE_FLAG);
        canvas.clipRect(0, startY, getMeasuredWidth(), endY);// left, top,
        canvas.drawText(mText, mTextStartX,
                getMeasuredHeight() / 2
                        - ((mPaint.descent() + mPaint.ascent()) / 2), mPaint);
        canvas.restore();
    }

    private void drawChangeLeft(Canvas canvas, int r) {
        drawText_h(canvas, mTextChangeColor, mTextStartX,
                (int) (mTextStartX + mProgress * mTextWidth));
    }

    private void drawOriginLeft(Canvas canvas, int r) {
        drawText_h(canvas, mTextOriginColor, (int) (mTextStartX + mProgress
                * mTextWidth), mTextStartX + mTextWidth);
    }

    private void drawChangeRight(Canvas canvas, int r) {
        drawText_h(canvas, mTextChangeColor,
                (int) (mTextStartX + (1 - mProgress) * mTextWidth), mTextStartX
                        + mTextWidth);
    }

    private void drawOriginRight(Canvas canvas, int r) {
        drawText_h(canvas, mTextOriginColor, mTextStartX,
                (int) (mTextStartX + (1 - mProgress) * mTextWidth));
    }

    private void drawChangeTop(Canvas canvas, int r) {
        drawText_v(canvas, mTextChangeColor, mTextStartY,
                (int) (mTextStartY + mProgress * mTextHeight));
    }

    private void drawOriginTop(Canvas canvas, int r) {
        drawText_v(canvas, mTextOriginColor, (int) (mTextStartY + mProgress
                * mTextHeight), mTextStartY + mTextHeight);
    }

    private void drawChangeBottom(Canvas canvas, int t) {
        drawText_v(canvas, mTextChangeColor,
                (int) (mTextStartY + (1 - mProgress) * mTextHeight),
                mTextStartY + mTextHeight);
    }

    private void drawOriginBottom(Canvas canvas, int t) {
        drawText_v(canvas, mTextOriginColor, mTextStartY,
                (int) (mTextStartY + (1 - mProgress) * mTextHeight));
    }

    public float getProgress() {
        return mProgress;
    }

    public void setProgress(float progress) {
        this.mProgress = progress;
        invalidate();
    }

    public int getTextSize() {
        return mTextSize;
    }

    public void setTextSize(int mTextSize) {
        this.mTextSize = mTextSize;
        mPaint.setTextSize(mTextSize);
        requestLayout();
        invalidate();
    }

    public void setText(String text) {
        this.mText = text;
        requestLayout();
        invalidate();
    }

    public String getText(){
        return this.mText;
    }

    public int getTextOriginColor() {
        return mTextOriginColor;
    }

    public void setTextOriginColor(int mTextOriginColor) {
        this.mTextOriginColor = mTextOriginColor;
        invalidate();
    }

    public int getTextChangeColor() {
        return mTextChangeColor;
    }

    public void setTextChangeColor(int mTextChangeColor) {
        this.mTextChangeColor = mTextChangeColor;
        invalidate();
    }

    private int dp2px(float dpVal) {
        return (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP,
                dpVal, getResources().getDisplayMetrics());
    }

    private int sp2px(float dpVal) {
        return (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_SP,
                dpVal, getResources().getDisplayMetrics());
    }

    private static final String KEY_STATE_PROGRESS = "key_progress";
    private static final String KEY_DEFAULT_STATE = "key_default_state";

    @Override
    protected Parcelable onSaveInstanceState() {
        Bundle bundle = new Bundle();
        bundle.putFloat(KEY_STATE_PROGRESS, mProgress);
        bundle.putParcelable(KEY_DEFAULT_STATE, super.onSaveInstanceState());
        return bundle;
    }

    @Override
    protected void onRestoreInstanceState(Parcelable state) {

        if (state instanceof Bundle) {
            Bundle bundle = (Bundle) state;
            mProgress = bundle.getFloat(KEY_STATE_PROGRESS);
            super.onRestoreInstanceState(bundle
                    .getParcelable(KEY_DEFAULT_STATE));
            return;
        }
        super.onRestoreInstanceState(state);
    }
}

   本文中肯定有很多没讲清楚的,有些话语可能也表达不妥,实现效果也没贴(暂时不会贴动态图),但亲测是能比较好地实现了今日头条中顶部导航栏效果的。