仿今日頭條頂部導航欄效果實現
最近在做一個專案的時候,需要實現像今天頭條那樣的頂部導航欄效果,通過在網上了解自定義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方法,導致字型變色錯亂的問題(具體可看鴻洋大神ColorTrackView這一篇部落格,裡面實現效果很好,但是當item多到超出了螢幕可顯示的範圍時,滑動的時候就滑不過去了,即使繼承了HorizontalScrollView ,當越過多個pager,點選item時,會出現字型顏色錯亂的問題,自己可測試一遍就知道了)。
下面是佈局檔案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);
}
}
關於ColorTrackView的style.xml可自行到鴻洋大神的ColorTrackView中檢視,同時也學習下如何實現字型變色的。
本文中肯定有很多沒講清楚的,有些話語可能也表達不妥,實現效果也沒貼(暫時不會貼動態圖),但親測是能比較好地實現了今日頭條中頂部導航欄效果的。最後由衷感謝鴻洋大神和風兮水寒提供了良好的參考。