close

這是 JsonChao 的第202期分享

最近公司的產品要進行改版,其中涉及到了登陸頁面的UI,需要把註冊和登陸功能換成ViewPager的頁面切換,本想利用下原生自帶的TabLayout作為ViewPager指示器就完事了,誰知UI設計在指示器上多了個小三角,雖然網上已經有封裝的很完善的ViewPager指示器,不過這個小東西確實沒什麼難度,就沒必要引入第三方了,順手自己寫了個,先來看下實現效果:

我準備從三部分來講1、ViewPager指示器的簡單實現2、ViewPager指示器的完整封裝3、ViewPager指示器的封裝使用(兩行代碼完成)

1、ViewPager指示器的實現

這部分其實很簡單,我們先不考慮其他過多的干擾因素,我們就單純的把它當成一個可跟隨ViewPager滑動的標題欄,來看下這張圖:

實現思路:1、首先這個頁面是由一個橫向排列的線性布局LinearLayout和ViewPager組成,其中這個線性布局LinearLayout里包含了多個大小一樣的TextView。2、再來我們去監聽這個ViewPager的滑動事件把對應位置上的文字進行高亮顯示即可。3、然後我們要做的就是在這個線性布局LinearLayou里去繪製小三角形,讓其也跟隨着ViewPager的移動,位置上的相關數據我們在監聽ViewPager的滑動事件里的回調方法中就可以得到,所以應該沒什麼大問題。

接下來我們分析下這個三角形的繪製:所需要的數據:三角形的底邊寬,高度,繪製的起始位置為了能夠適應不同屏幕大小,這裡我們就不對三角形的底邊寬和高度進行固定值寫死了,我們通過測量的方式去得到,然後用閉合路徑Path去勾勒。三角形的底邊寬:我們取Tab寬度的六分之一(屏幕寬度/可見Tab數量/6)三角形的高度:我們取底邊寬的一半(屏幕寬度/可見Tab數量/6/2)三角形繪製的起始位置:第一個Tab的中點減去底邊寬度的一半(屏幕寬度/可見Tab數量/2-屏幕寬度/可見Tab數量/6/2)附加:怎麼確定三角形跟隨着ViewPager滑動的位置呢?在ViewPager的滑動監聽回調接口中的onPageScrolled里的positionOffset參數已經給我們提供好了,它代表着頁面滑動的偏移值,區間在[0,1),也就是當前頁面的滑動距離為:Tab的寬度*偏移值,這樣三角形的某時某刻的x軸位置也就確定了,即初始位置+偏移距離

/** * This method will be invoked when the current page is scrolled, either as part * of a programmatically initiated smooth scroll or a user initiated touch scroll. * * @param position Position index of the first page currently being displayed. * Page position+1 will be visible if positionOffset is nonzero. * @param positionOffset Value from [0, 1) indicating the offset from the page at position. * @param positionOffsetPixels Value in pixels indicating the offset from position. */ void onPageScrolled(int position, float positionOffset, int positionOffsetPixels);

這裡是簡單版本的代碼實現(固定三個Tab頁):

<?xml version="1.0" encoding="utf-8"?><LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:tools="http://schemas.android.com/tools" android:id="@+id/activity_main" android:layout_width="match_parent" android:layout_height="match_parent" android:orientation="vertical"> <com.lcw.viewpagertriangleindicator.ViewPagerTriangleIndicator android:id="@+id/vpti_main_tab" android:layout_width="match_parent" android:layout_height="45dp" android:background="@color/colorAccent" android:orientation="horizontal"> <TextView android:layout_width="0dp" android:layout_height="match_parent" android:layout_weight="1" android:gravity="center" android:text="新聞" android:textColor="#FFFFFF" /> <TextView android:layout_width="0dp" android:layout_height="match_parent" android:layout_weight="1" android:gravity="center" android:text="音樂" android:textColor="#FFFFFF" /> <TextView android:layout_width="0dp" android:layout_height="match_parent" android:layout_weight="1" android:gravity="center" android:text="遊戲" android:textColor="#FFFFFF" /> </com.lcw.viewpagertriangleindicator.ViewPagerTriangleIndicator> <android.support.v4.view.ViewPager android:id="@+id/vp_main_content" android:layout_width="match_parent" android:layout_height="match_parent" /></LinearLayout>/** * 自定義ViewPager指示器(三角形) * Create by: chenwei.li * Date: 2017/7/16 * Time: 下午1:39 * Email: lichenwei.me@foxmail.com */public class ViewPagerTriangleIndicator extends LinearLayout { private int mTriangleWidth;//三角形底邊寬 private int mTriangleHeigh;//三角形高度 private int mTriangleInitPos;//三角形起始點 private int mTriangleMoveWidth;//三角形移動偏移 private Paint mPaint; private Path mPath; public ViewPagerTriangleIndicator(Context context) { super(context, null); } public ViewPagerTriangleIndicator(Context context, AttributeSet attrs) { super(context, attrs); initPaint(); } /** * 初始化畫筆 */ private void initPaint() { mPaint = new Paint(); mPaint.setColor(Color.parseColor("#FFFFFF")); mPaint.setAntiAlias(true); mPaint.setStyle(Paint.Style.FILL); } /** * 初始化三角形 */ private void initTriangle() { mPath = new Path(); mPath.moveTo(0, 0); mPath.lineTo(mTriangleWidth, 0); mPath.lineTo(mTriangleWidth / 2, -mTriangleHeigh); mPath.close(); } /** * 當布局大小發生變化時回調 * * @param w * @param h * @param oldw * @param oldh */ @Override protected void onSizeChanged(int w, int h, int oldw, int oldh) { super.onSizeChanged(w, h, oldw, oldh); mTriangleWidth = w / 3 / 6; mTriangleHeigh = mTriangleWidth / 2 - 12; mTriangleInitPos = getScreenWidth() / 3 / 2 - mTriangleWidth / 2; initTriangle(); } /** * 繪製子View * * @param canvas */ @Override protected void dispatchDraw(Canvas canvas) { super.dispatchDraw(canvas); canvas.translate(mTriangleInitPos + mTriangleMoveWidth, getHeight()); canvas.drawPath(mPath, mPaint); } /** * 監聽ViewPager滑動,聯動Indicator * * @param position * @param positionOffset */ protected void scroll(int position, float positionOffset) { int tabWidth = getScreenWidth() / 3; mTriangleMoveWidth = (int) (tabWidth * position + tabWidth * positionOffset); invalidate(); } /** * 獲取屏幕寬度 * * @return */ private int getScreenWidth() { WindowManager windowManager = (WindowManager) getContext().getSystemService(Context.WINDOW_SERVICE); DisplayMetrics displayMetrics = new DisplayMetrics(); windowManager.getDefaultDisplay().getMetrics(displayMetrics); return displayMetrics.widthPixels; }}public class MainActivity extends AppCompatActivity { private ViewPager mViewPager; private ViewPagerTriangleIndicator mViewPagerTriangleIndicator; private List<String> mTitles = Arrays.asList("新聞", "音樂", "遊戲"); private List<Fragment> mFragments = new ArrayList<Fragment>(); @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); mViewPager = (ViewPager) findViewById(R.id.vp_main_content); mViewPagerTriangleIndicator = (ViewPagerTriangleIndicator) findViewById(R.id.vpti_main_tab); //創建Fragment for (String title : mTitles) { SimpleFragmet simpleFragmet = SimpleFragmet.newInstance(title); mFragments.add(simpleFragmet); } //設置適配器 mViewPager.setAdapter(new FragmentPagerAdapter(getSupportFragmentManager()) { @Override public Fragment getItem(int position) { return mFragments.get(position); } @Override public int getCount() { return mFragments.size(); } }); //添加滑動監聽 mViewPager.addOnPageChangeListener(new ViewPager.OnPageChangeListener() { @Override public void onPageScrolled(int position, float positionOffset, int positionOffsetPixels) { mViewPagerTriangleIndicator.scroll(position, positionOffset); } @Override public void onPageSelected(int position) { } @Override public void onPageScrollStateChanged(int state) { } }); }}

看下效果圖:

2、ViewPager指示器的封裝

在上面我們已經實現了簡單版本的ViewPager帶三角形的滑動指示器實現,接下來我們需要對其進行封裝,讓它變得更加簡單易用。首先我們來分析下簡單版本的不足:1、Tab數量是固定的,每次都需要在XML里編寫TextView,太過繁瑣,當Tab數量很多的時候,並且weight都是1時,就會擠在一屏幕里。2、調用太複雜,需要在Activity里去實現滑動監聽,並調用三角形聯動。3、其他一些小細節,比如當Tab頁只有1頁或者2頁的時候,指示器三角形會變得很大,影響美觀等。帶着這些問題,我們開啟ViewPagerTriangleIndicator的優化之旅吧!

首先我們需要一個當前頁面可顯示的最大Tab數,也就是屏幕可見區域的最大Tab數,這裡引入我們的自定義屬性(關於自定義屬性的使用,這裡就不再做過多闡述):

<?xml version="1.0" encoding="utf-8"?><resources> <attr name="visible_tab_num" format="integer" /> <declare-styleable name="ViewPagerTriangleIndicator"> <attr name="visible_tab_num" /> </declare-styleable></resources> /** * 獲取自定義屬性值(獲取xml設置最大可見Tab數量) * * @param context * @param attrs */ private void initAttr(Context context, AttributeSet attrs) { TypedArray typedArray = context.obtainStyledAttributes(attrs, R.styleable.ViewPagerTriangleIndicator); if (typedArray != null) { mVisibleTabNum = typedArray.getInt(R.styleable.ViewPagerTriangleIndicator_visible_tab_num, VISIBLE_COUNT_NUM); } }

通過上面的代碼我們就可以拿到在XML文件中visible_tab_num的值了,根據這個值我們按照上文提到的思路就可以得到一些數據,比如三角形的底邊寬,高度,起始位置等。需要注意的是當前的Tab的寬度就需要動態去計算了,我們根據屏幕寬度/可見Tab數,就可以得到每個Tab的寬度,我們在XML加載完畢的時候去進行動態的修改。

/** * 在XML布局加載完畢後回調 */ @Override protected void onFinishInflate() { super.onFinishInflate(); //根據可顯示的Tab數量動態去改變Tab的寬度 int totalTabNum = getChildCount(); if (mVisibleTabNum != 0 && totalTabNum != 0) { for (int i = 0; i < totalTabNum; i++) { View view = getChildAt(i); LinearLayout.LayoutParams layoutParams = (LinearLayout.LayoutParams) view.getLayoutParams(); layoutParams.weight = 0; layoutParams.width = getScreenWidth() / mVisibleTabNum; view.setLayoutParams(layoutParams); } } }

還有,我們還需要對scroll方法進行一些改造:

/** * 監聽ViewPager滑動,聯動Indicator * * @param position * @param positionOffset */ protected void scroll(int position, float positionOffset) { int tabWidth = getScreenWidth() / mVisibleTabNum; mTriangleMoveWidth = (int) (tabWidth * position + tabWidth * positionOffset); if ((mVisibleTabNum - 2) <= position && positionOffset > 0 && getChildCount() > mVisibleTabNum) { this.scrollTo((int) ((position - (mVisibleTabNum - 2)) * tabWidth + tabWidth * positionOffset), 0); } invalidate(); }

當Tab數量超出一頁的時候,隨着三角形的滑動我們也需要對Tab進行一個滑動處理,這裡舉個例子:當屏幕可見Tab數為4時,那麼Tab從第3個滑動到第4個的時候,需要同時把第1個Tab往左移出屏幕,此時滑動距離相對於整個指示器為1個Tab的距離。當屏幕可見Tab數為4時,那麼Tab從第4個滑動到第5個的時候,需要同時把第2個Tab往左移出屏幕,此時滑動距離相對於整個指示器為2個Tab的距離。哈哈,是不是有點繞,沒想明白的朋友拿筆在紙上吧,上面的判斷也是由此得出。

好了,接下來就要開始解決我們的問題了。解決問題1:既然ViewPager指示器是繼承於線性布局LinearLayout,那也就是ViewGroup,我們很自然的可以想到addView這個方法,所以我們只需要讓其接收一個List的集合對象並根據集合元素的文字信息動態添加子View即可,實現代碼如下:

/** * 設置指示器標題並給標題添加監聽事件 * * @param titles */ public void setPageTitle(List<String> titles) { this.mTitles = titles; if (mTitles != null && mTitles.size() > 0) { removeAllViews(); for (int i = 0; i < mTitles.size(); i++) { TextView textView = new TextView(getContext()); LinearLayout.LayoutParams layoutParams = new LayoutParams(getScreenWidth() / mVisibleTabNum, LayoutParams.MATCH_PARENT); layoutParams.width = getScreenWidth() / mVisibleTabNum; textView.setText(mTitles.get(i)); textView.setGravity(Gravity.CENTER); textView.setTextSize(TypedValue.COMPLEX_UNIT_SP, 16); textView.setTextColor(Color.parseColor("#FFFFFF")); textView.setLayoutParams(layoutParams); addView(textView); } } }

解決問題2:我們可以在ViewPager指示器內部去持有外部ViewPager的引用並且去實現滑動監聽即可:

/** * 綁定ViewPager * * @param viewPager */ public void setViewPagerWithIndicator(ViewPager viewPager) { this.mViewPager = viewPager; mViewPager.addOnPageChangeListener(new ViewPager.OnPageChangeListener() { @Override public void onPageScrolled(int position, float positionOffset, int positionOffsetPixels) { if (mAddPageChangeListener != null) { mAddPageChangeListener.onPageScrolled(position, positionOffset, positionOffsetPixels); } scroll(position, positionOffset); } @Override public void onPageSelected(int position) { setInitPageTitlesColor(); setPageTitleHighColor(position); mViewPager.setCurrentItem(position); if (mAddPageChangeListener != null) { mAddPageChangeListener.onPageSelected(position); } } @Override public void onPageScrollStateChanged(int state) { if (mAddPageChangeListener != null) { mAddPageChangeListener.onPageScrollStateChanged(state); } } }); }

這裡大家可能會注意到mAddPageChangeListener這個變量,這裡是重寫了原生自帶addOnPageChangeListener的一個回調函數,因為我們在內部去實現了addOnPageChangeListener監聽,當外部的ViewPager再去實現滑動監聽時,此時就會把我們內部的實現方法覆蓋,也就是會導致 scroll(position, positionOffset);方法失效,所以這裡我們需要再去定義一個滑動監聽器即可,並且提供註冊監聽器方法。

/** * 複製官方addOnPageChangeListener對應的接口方法 * Callback interface for responding to changing state of the selected page. */ public interface AddPageChangeListener { /** * This method will be invoked when the current page is scrolled, either as part * of a programmatically initiated smooth scroll or a user initiated touch scroll. * * @param position Position index of the first page currently being displayed. * Page position+1 will be visible if positionOffset is nonzero. * @param positionOffset Value from [0, 1) indicating the offset from the page at position. * @param positionOffsetPixels Value in pixels indicating the offset from position. */ void onPageScrolled(int position, float positionOffset, int positionOffsetPixels); /** * This method will be invoked when a new page becomes selected. Animation is not * necessarily complete. * * @param position Position index of the new selected page. */ void onPageSelected(int position); /** * Called when the scroll state changes. Useful for discovering when the user * begins dragging, when the pager is automatically settling to the current page, * or when it is fully stopped/idle. * * @param state The new scroll state. * @see ViewPager#SCROLL_STATE_IDLE * @see ViewPager#SCROLL_STATE_DRAGGING * @see ViewPager#SCROLL_STATE_SETTLING */ void onPageScrollStateChanged(int state); } /** * 避免用戶監聽ViewPager複寫了addOnPageChangeListener使得三角形滑動效果失效 * * @param addPageChangeListener */ public void addPageChangeListener(AddPageChangeListener addPageChangeListener) { this.mAddPageChangeListener = addPageChangeListener; }

解決問題3:這裡我們只需要去定義一個三角形底部寬度的默認最大值即可,這個默認值可以是屏幕寬度的三分之一的六分之一,也就是當有3個Tab時候,三角形底部的寬度。

private int DEFAULT_TRIANGLE_WIDTH = getScreenWidth() / 3 / 6;//最大三角形底邊寬度 if (mTriangleWidth > DEFAULT_TRIANGLE_WIDTH) { mTriangleWidth = DEFAULT_TRIANGLE_WIDTH; }

再來我們還可以去做一些事情,比如設置Tab的文字默認顏色,高亮顏色,監聽事件等。

3、ViewPager指示器的使用

經過我們封裝之後,我們的調用就非常簡單了,只需要短短的2行代碼:

//設置指示器標題 mViewPagerTriangleIndicator.setPageTitle(mTitles); //綁定ViewPagermViewPagerTriangleIndicator.setViewPagerWithIndicator(mViewPager);

好了,文章到這裡就結束了,由於篇幅限制,這裡不能對一些東西講的太細,比如一些自定義View的基礎,大家自行查閱相關資料哈。

END


往期推薦



這兩年,我打造了一份具備競爭壁壘的 Android 性能優化 通關秘籍

面試為什麼都需要基礎紮實,有深度的候選人?

(原創升級版)構建一份提升學習效率 99% 的私藏秘籍

技術人如何讓自己更值錢?

分享一份我打磨了兩年的面試題庫

點擊下方卡片關注JsonChao,為你構建一套

未來技術人必備的底層能力系統


▲點擊上方卡片關注JsonChao,構建一套

未來Android開發必備的知識體系

歡迎把文章分享到朋友圈

年度成長社群

arrow
arrow
    全站熱搜
    創作者介紹
    創作者 鑽石舞台 的頭像
    鑽石舞台

    鑽石舞台

    鑽石舞台 發表在 痞客邦 留言(0) 人氣()