最近在做一个项目的时候,需要实现像今天头条那样的顶部导航栏效果,通过在网上了解自定义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);
}
}
本文中肯定有很多没讲清楚的,有些话语可能也表达不妥,实现效果也没贴(暂时不会贴动态图),但亲测是能比较好地实现了今日头条中顶部导航栏效果的。