Android 高仿華為手機Tab頁滑動導航效果
首先帶大家看一下實現效果,用了兩種實現方式:
1.基於LinearLayout實現,導覽列不可響應手指滑動
2.基於HorizontalScrollView實現,導覽列可響應手指滑動
實現方式雖然不一樣,但是使用的是一樣的,因為我介面封裝的一模一樣,下面看實現效果。
基於LinearLayout的實現:
基於HorizontalScrollView的實現:
兩者效果一樣,區別就在於導航條可否隨使用者操作滑動。
下面只說明LinearLayout實現,HorizontalScrollView僅僅是套了一層LinearLayout,因為它只允許套一個子控制項。
需求開發點:
1.自訂控制項的編寫(通用方法:建構函式、init()、onMeasure、onLayout、onDraw、dispatchDraw)
2.白色圓點的位移量的計算
3.當Tab頁超過5個的時候,白點和LinearLayout內容的組合滑動,達到白點不動,而文字移動的效果。
實現這三點,這個控制項就算完事,當然一些細節方面的,可以根據自己的需求去調優。
1.自訂控制項的編寫
1) 這裡主要是onMeasure方法的複寫,不能用預設的onMeasure,需要我們根據Tab的數量去計算,是1-6個Tab的measure結果
這個就是根據Tab的數量平分螢幕的寬,當數量超過5個的時候,為了保持效果,只顯示5個,超出的內容將其追加到螢幕外面,螢幕是有界的,視圖是無界的,你可以無限往右邊添加,只要記憶體能容下,可以用個for迴圈無限添加看看發生什麼。
2) 複寫dispatchDraw方法,這裡dispatchDraw方法的作用就是畫一個白色的圓點。
白色圓點的位移量的計算
這個得靠ViewPager的OnPageChangeListener,通過它onPageScrolled的回調去計算,計算好了調用invalidate()去重繪。
當Tab頁超過5個的時候,白點和LinearLayout內容的組合滑動,達到白點不動,而文字移動的效果。
當滑動到第5個Tab,如果只是白色圓點移動就會移動到螢幕外面去,這個時候也需要讓LinearLayout內容聯動,通過scrollTo方法,往與白點相反的方向移動內容,使其和白點的移動達到平衡,這樣白點不動,上面的標題內容移動。
下面分析,寫該控制項需要的一些變數,先看:
1.需要白點是吧,我們用畫筆畫,所以Paint需要
2.白點畫多大?所以需要一個白點的半徑變數radius
3.白點的位移量,我們定義一個變數mOffset,根據ViewPager的滑動動態計算
4.上面還有一排文字,用TextView實現,這個TextView是根據Tab的數量動態添加的,數量不可控,我這裡並沒有定義成變數。但是文字內容對應的是一個Fragment的標題,也就是說標題和Fragment是一一對應的關係。OK用Map。
/** * 存放Tab的集合 Key:導覽列的標題 Fragment:導航頁 */ private Map mTabs;
TextView就是根據mTabs的數量動態添加的。
5.我們怎麼去計算滑動位移量呢?這個得靠ViewPager的OnPageChangeListener,通過它onPageScrolled的回調去計算mOffset,就是通過這種計算圓點的位移,這裡我持有了一個ViewPager的引用,所以有變數mViewPager,其實要不要關係不大.
6.螢幕最大可顯示的Tab數量MAX_VISIBLE_TAB_COUNTS,總不能有100個你就顯示100個吧.
7.實際螢幕上可顯示的Tab數量mRealVisibleTabCounts
8.螢幕寬度mScreenWidth輔助onMeasure計算的
下面是實現代碼:
package com.csm.hwtab;import java.util.ArrayList;import java.util.LinkedHashMap;import java.util.List;import java.util.Map;import android.annotation.SuppressLint;import android.content.Context;import android.graphics.Canvas;import android.graphics.Color;import android.graphics.Paint;import android.graphics.Paint.Cap;import android.graphics.Paint.Style;import android.support.v4.app.Fragment;import android.support.v4.view.ViewPager;import android.support.v4.view.ViewPager.OnPageChangeListener;import android.util.AttributeSet;import android.util.DisplayMetrics;import android.util.TypedValue;import android.view.Gravity;import android.view.View;import android.view.View.OnClickListener;import android.widget.LinearLayout;import android.widget.TextView;/** * @author Rander.C */@SuppressLint("NewApi")public class TabLinearLayout extends LinearLayout implements OnClickListener, OnPageChangeListener { /** * 最大可顯示的Tab數,可寫成自訂屬性在xml裡面配置 */ private static final int MAX_VISIBLE_TAB_COUNTS = 5; /** * 實際可見的Tab數,比如有8個Tab,超過8個,為了保證效果,最多顯示5個 * 此時mRealVisibleTabCounts為5 * 如果只有3個tab小於5個,則mRealVisibleTabCounts為3 * 並根據這個去計算每個tab的寬度,螢幕寬度除以mRealVisibleTabCounts嘛 */ private int mRealVisibleTabCounts; /** * 小圓點滑動位移量 */ private float mOffset; /** * 繪製小圓點畫筆 */ private Paint mPaint; /** * 小圓點半徑 */ private int mCircleRadius; /** * 螢幕寬度 */ private int mScreenWidth; /** * 存放Tab的集合 Key:導覽列的標題 Fragment:導航頁 */ private Map mTabs; /** * View中有getContext()方法,用變數就會 * 多一個Context引用,會增加一丁點虛擬機器的負擔嗎? * 如果不用變數,但是每次都使用getContext(),是不是都要多一層 * 調用棧,這個怎麼權衡,求答案,還是我想多了? */ private Context mContext; private ViewPager mViewPager; public TabLinearLayout(Context context, AttributeSet attrs) { this(context, attrs, -1); } public TabLinearLayout(Context context) { this(context, null); } public TabLinearLayout(Context context, AttributeSet attrs, int defStyleAttr) { super(context, attrs, defStyleAttr); init(); } private void init() { mContext = getContext(); setOrientation(HORIZONTAL); mPaint = new Paint(); mPaint.setAntiAlias(true); mPaint.setDither(true); mPaint.setStyle(Style.FILL); mPaint.setColor(Color.WHITE); mPaint.setStrokeCap(Cap.ROUND); DisplayMetrics metrics = getResources().getDisplayMetrics(); mScreenWidth = metrics.widthPixels; mCircleRadius = (int) getResources().getDimension(R.dimen.radius); } @Override protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { super.onMeasure(widthMeasureSpec, heightMeasureSpec); int tabCount = getChildCount(); if (tabCount == 0) { return; } /** 不管預設測量結果怎麼樣,我們都要重新給其寬度賦值*/ mRealVisibleTabCounts = MAX_VISIBLE_TAB_COUNTS < tabCount ? MAX_VISIBLE_TAB_COUNTS : tabCount; // 為每一個子view重新分配mScreenWidth / mRealVisibleTabCounts大小的寬度 int averageWidth = mScreenWidth / mRealVisibleTabCounts; for (int i = 0; i < tabCount; i++) { View view = getChildAt(i); LinearLayout.LayoutParams params = (android.widget.LinearLayout.LayoutParams) view.getLayoutParams(); params.weight = 0; params.width = averageWidth; view.setLayoutParams(params); } } /** * 獲得Tabs列表 * @return */ public List getTabs() { return new ArrayList(mTabs.values()); } /** * 添加一個Tab項,添加一個tab的時候同時給 * 該視圖添加一個TextView * @param title * @param tab */ public void addTab(String title, Fragment tab) { if (mTabs == null) { mTabs = new LinkedHashMap<>(); } mTabs.put(title, tab); addView(createTabItem(title, mTabs.size() - 1)); } /** * 設定ViewPager * 同時給其設定OnPageChangeListener監聽器。 * @param viewPager */ public void setViewPager(ViewPager viewPager) { mViewPager = viewPager; mViewPager.setOnPageChangeListener(this); } /** * 建立一個Tab視圖 * @param title Fragment對應的標題 * @param index 該Fragment對應的索引下標 * @return 已經構造好的tab視圖 */ private View createTabItem(String title, int index) { TextView tv_title = new TextView(mContext); tv_title.setText(title); tv_title.setTag(index); tv_title.setGravity(Gravity.CENTER); tv_title.setTextColor(Color.WHITE); tv_title.setTextSize(TypedValue.COMPLEX_UNIT_SP, 18); tv_title.setOnClickListener(this); return tv_title; } @Override public void onClick(View v) { int tabIndex = (Integer) v.getTag(); if (onTabOperatorListener != null) { onTabOperatorListener.onTabClick(tabIndex); } mViewPager.setCurrentItem(tabIndex); } @Override public void onPageScrollStateChanged(int state) { if (null != onTabOperatorListener) { onTabOperatorListener.onPageScrollStateChanged(state); } } @Override public void onPageScrolled(int sourcePosition, float positionOffset, int positionOffsetPixels) { mRealVisibleTabCounts = MAX_VISIBLE_TAB_COUNTS < getChildCount() ? MAX_VISIBLE_TAB_COUNTS : getChildCount(); int tabWidth = getWidth() / mRealVisibleTabCounts; // 計算小圓點的位移量,這個可以畫圖理解 mOffset = (int) (tabWidth / 2 + sourcePosition * tabWidth + positionOffset * tabWidth ); // 如果滾動到最後一個,則同時要向左滾動tab內容,這種情況是:原點在向右移動,同時整體又向做滑動,兩個滑動相互抵消,相當於原點沒有移動 if (getChildCount() > mRealVisibleTabCounts && positionOffset > 0 && sourcePosition >= mRealVisibleTabCounts - 1) { scrollTo((int) ((sourcePosition + 1 - mRealVisibleTabCounts) * tabWidth + tabWidth * positionOffset), 0); } invalidate(); if (null != onTabOperatorListener) { onTabOperatorListener.onTabScrolled(sourcePosition, positionOffset, positionOffsetPixels); } } @Override public void onPageSelected(int index) { if (null != onTabOperatorListener) { onTabOperatorListener.onTabSelected(index); } /** * 這裡不要多想,只是為了保證選中的tab為第一個時,讓布局重設到原點 */ if(index == 0) { scrollTo(0, 0); } } /** * Tab操作相關的監聽器 * @author rander */ public interface OnTabOperatorListener { void onTabClick(int tabIndex); void onTabSelected(int tabIndex); void onTabScrolled(int sourcePosition, float positionOffset, int positionOffsetPixels); void onPageScrollStateChanged(int state); } public OnTabOperatorListener onTabOperatorListener; public void setOnTabItemClickListener(OnTabOperatorListener onTabOperatorListener) { this.onTabOperatorListener = onTabOperatorListener; } @Override protected void dispatchDraw(Canvas canvas) { super.dispatchDraw(canvas); //如果只有一個Tab則,不繪製圓點 if (getChildCount() <= 1) { return; } //根據mOffset繪製位移量 canvas.drawCircle(mOffset, getHeight() - mCircleRadius * 2, mCircleRadius, mPaint); }}
關於位移量的計算畫圖理解:
位移量計算的運算式:
mOffset = (int) (tabWidth / 2 + sourcePosition * tabWidth + positionOffset * tabWidth );
圖解:
第一個豎線和第二個豎線:tabWidth / 2
第二個豎線和第三個豎線:sourcePosition * tabWidth
第三個豎線和第四個豎線:pZ喎?http://www.bkjia.com/kf/ware/vc/" target="_blank" class="keylink">vc2l0aW9uT2Zmc2V0ICogdGFiV2lkdGg8YnIgLz4NCtK7z+C8077NysfGq9LGwb/ByyzV4rj20qrV0rW9c291cmNlUG9zaXRpb266zXBvc2l0aW9uT2Zmc2V0tcS55sLJvs26w7zGy+PGq9LGwb/By6Osc2Nyb2xsVG+1xMar0sbBv9Kyv8nS1LLO1dXV4rj2wO294qGjPC9wPg0KPHA+vdPPwsC0vs3Kx8q508PByy48c3Ryb25nPsfzTGludXjPwrXEusO1xLutzby5pL7fPC9zdHJvbmc+PC9wPg0KPHA+yunQtLK8vtZ0YWIueG1so7o8L3A+DQo8cHJlIGNsYXNzPQ=="brush:java;">
在Activity中使用:
package com.csm.hwtab;import com.csm.hwtab.adapter.MyFragmentPagerAdapter;import com.csm.hwtab.fragment.OneFragment;import android.os.Bundle;import android.support.v4.app.FragmentActivity;import android.support.v4.view.ViewPager;import android.view.Window;public class TestActivity extends FragmentActivity{ private ViewPager mViewPager; private TabLinearLayout mTabview; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); requestWindowFeature(Window.FEATURE_NO_TITLE); setContentView(R.layout.tab); initTabsView(); } private void initTabsView() { mViewPager = (ViewPager) findViewById(R.id.view_pager); mTabview = (TabLinearLayout)findViewById(R.id.tab); mTabview.addTab("先秦0", new OneFragment()); mTabview.addTab("先秦1", new OneFragment()); mTabview.addTab("先秦2", new OneFragment()); mTabview.addTab("先秦3", new OneFragment()); mTabview.addTab("先秦4", new OneFragment()); mTabview.addTab("先秦5", new OneFragment()); mTabview.addTab("先秦6", new OneFragment()); mViewPager.setAdapter(new MyFragmentPagerAdapter(getSupportFragmentManager(), mTabview.getTabs())); mTabview.setViewPager(mViewPager); mViewPager.setCurrentItem(0); }}
效果就如同上面的效果一樣了。
github:https://github.com/shuangmin/HwTab