開源庫系列:FlycoTabLayout 學習
寫在前面
本文主要目的是全方位學習 ofollow,noindex">FlycoTabLayout 庫程式碼,整體功能比較簡單和實用,很多專案都有切Tab效果,所以這裡給大家推薦下,同時我也會細緻的講解這個庫的實現,認真分析裡面用到的知識點,最後基於自己的思考擴充套件另一種實現方式。 https://github.com/whiskeyfei/FlycoTabLayout
目錄
以下內容為 FlycoTabLayout 庫中使用到的知識點,只需要簡單瞭解每個知識點的用法,就能看懂並且自己也能獨立開發類似的控制元件。注意:我這裡只會稍微介紹下含義和使用方法,更深入的研究希望大家自己去學習。

目錄
看完上圖,讓我猜猜你的目的?
1、你想直接用原控制元件?
FlycoTabLayout 倉庫 使用細節 README 中已經很詳細了,看著程式碼例項也能快速上手。
2、你想直接用此控制元件?
簡單看下圖


image.png

原倉庫很多效果都整合到一起了,實際應用場景可能不會覆蓋這麼多功能,所以我進行了功能拆分,以下控制元件任你選,控制元件程式碼都進行單獨拆分過了,程式碼看起來也比較簡單易懂,大家說好用才好用。
控制元件 | 功能 |
---|---|
HintImageView.java | StateListDrawable 實現三種狀態 |
ShapeTextTabLayout.java | 文字切換 |
SingleTextNewTabLayout.java | 文字切換( Adapter實現) |
LineTabLayout.java" target="_blank" rel="nofollow,noindex">SingleUnderLineTabLayout.java | 文字+下劃線切換 |
SingleIconTextTabLayout.java | 文字+icon切換 |
SingleIconTabLayout.java | icon切換 |
SingleIconTabLayout.java | icon+訊息切換 |
ShapeTextTabLayout.java | 文字+shape背景切換 |
SinglePointTabLayout.java | 文字+原點切換 |
SingleRectTabLayout.java | 文字+矩形塊切換 |
3、你想大概瞭解下控制元件?
知識點 :
自定義屬性、自定義View、動畫、GradientDrawable、StateListDrawable
基本思路:
1、自定義一個橫向線性父佈局
2、根據資料個數向父佈局中新增子View
3、文字和icon狀態通過迴圈遍歷來改變。
4、通過動畫來持續觸發 onDraw()方法,來繪製指示器位置。
你想全面瞭解程式碼思路?
請認真檢視文字以下內容和程式碼註釋。
一、作用
底部Tab切換、訊息個數展示、更新狀態展示。
二、Android 自定義屬性
自定義屬性通常用於自定義 View 控制元件開發,通過配置相關屬性達到某種效果.本節知識點 AttributeSet、TypedArray、styleable
屬性資源
屬性資原始檔放在 /res/values 目錄下,屬性資原始檔的根標籤時 <resources><resources/>,包含兩個子元素。
1、attr:定義一個屬性。
2、declare-styleable:定義一個 styleable 物件,每個styleable 物件就是一組 attr 屬性的集合。
當定義好了屬性資原始檔之後就可以在自定義控制元件中的構造方法中通過 AttributeSet 物件來獲取這些屬性了。
整體總結下各個名詞解釋:
名詞 | 含義 |
---|---|
AttributeSet | 引數的集合 |
TypedArray | 配合AttributeSet引數使用,簡化獲取方式 |
attr | 自定義attr標籤 |
declare-styleable | 定義一個styleable組,方便操作及複用 |
一般步驟如下:
- 首先自定義一個控制元件,可以是View、ViewGroup 等級別
- 在 attrs.xml 檔案中新增自己 styleable 和 item 等標籤元素
- 在包含自定義控制元件的 layout 中使用自定義屬性
- 在自定義控制元件構造方法中通過TypedArray獲取屬性,做相關操作
自定義Attributes例項:
<resources> <declare-styleable name="PieChart"> <attr name="showText" format="boolean" /> <attr name="labelPosition" format="enum"> <enum name="left" value="0"/> <enum name="right" value="1"/> </attr> </declare-styleable> </resources>
自定義 View 使用
<?xml version="1.0" encoding="utf-8"?> <LinearLayout xmlns:android="[http://schemas.android.com/apk/res/android](http://schemas.android.com/apk/res/android)" xmlns:custom="[http://schemas.android.com/apk/res/com.example.customviews](http://schemas.android.com/apk/res/com.example.customviews)"> <com.example.customviews.charting.PieChart custom:showText="true" custom:labelPosition="left" /> </LinearLayout>
自定義屬性需要一個特殊的名稱空間,例如:
http://schemas.android.com/apk/res/[your package name]
使用:
public PieChart(Context context, AttributeSet attrs) { super(context, attrs); TypedArray a = context.getTheme().obtainStyledAttributes(attrs, R.styleable.PieChart,0, 0); try { mShowText = a.getBoolean(R.styleable.PieChart_showText, false); mTextPos = a.getInteger(R.styleable.PieChart_labelPosition, 0); } finally { a.recycle(); } }
專案中使用
<declare-styleable name="CommonTabLayout"> <!-- indicator --> <attr name="tl_indicator_color"/> <attr name="tl_indicator_height"/> <attr name="tl_indicator_width"/> <attr name="tl_indicator_margin_left"/> <attr name="tl_indicator_margin_top"/> <attr name="tl_indicator_margin_right"/> <attr name="tl_indicator_margin_bottom"/> <attr name="tl_indicator_corner_radius"/> <attr name="tl_indicator_gravity"/> <attr name="tl_indicator_style"/> <attr name="tl_indicator_anim_enable"/> <attr name="tl_indicator_anim_duration"/> <attr name="tl_indicator_bounce_enable"/> <!-- underline --> <attr name="tl_underline_color"/> <attr name="tl_underline_height"/> <attr name="tl_underline_gravity"/> <!-- divider --> <attr name="tl_divider_color"/> <attr name="tl_divider_width"/> <attr name="tl_divider_padding"/> <!-- tab --> <attr name="tl_tab_padding"/> <attr name="tl_tab_space_equal"/> <attr name="tl_tab_width"/> <!-- title --> <attr name="tl_textsize"/> <attr name="tl_textSelectColor"/> <attr name="tl_textUnselectColor"/> <attr name="tl_textBold"/> <attr name="tl_textAllCaps"/> <!-- icon --> <!-- 設定icon寬度 --> <attr name="tl_iconWidth" format="dimension"/> <!-- 設定icon高度 --> <attr name="tl_iconHeight" format="dimension"/> <!-- 設定icon是否可見 --> <attr name="tl_iconVisible" format="boolean"/> <!-- 設定icon顯示位置,對應Gravity中常量值 --> <attr name="tl_iconGravity" format="enum"> <enum name="LEFT" value="3"/> <enum name="TOP" value="48"/> <enum name="RIGHT" value="5"/> <enum name="BOTTOM" value="80"/> </attr> <!-- 設定icon與文字間距 --> <attr name="tl_iconMargin" format="dimension"/> </declare-styleable> //自定義訊息view <declare-styleable name="MsgView"> <!-- 圓角矩形背景色 --> <attr name="mv_backgroundColor" format="color"/> <!-- 圓角弧度,單位dp--> <attr name="mv_cornerRadius" format="dimension"/> <!-- 圓角弧度,單位dp--> <attr name="mv_strokeWidth" format="dimension"/> <!-- 圓角邊框顏色--> <attr name="mv_strokeColor" format="color"/> <!-- 圓角弧度是高度一半--> <attr name="mv_isRadiusHalfHeight" format="boolean"/> <!-- 圓角矩形寬高相等,取較寬高中大值--> <attr name="mv_isWidthHeightEqual" format="boolean"/> </declare-styleable>
三、Android 自定義 View
本節知識點:Rect、Paint、Path、drawRect、drawLine、drawPath
說到自定義View 就不能不說自定義屬性,如果不熟悉可以檢視上一節,關於自定義三部曲就不展開,主要會介紹關於onDraw() 中 Canvas 在專案中使用到的知識點。
基礎概念
名詞 | 含義 |
---|---|
Rect | 定義一個矩形,用座標表示. |
Paint | 可以理解為 Canvas 上的畫筆,用來設定繪製風格,包括顏色、畫筆粗細、填充顏色、幾何圖形、文字、點陣圖等. |
Path | 多種型別路徑封裝組合,簡單理解為繪製圖形的外部輪廓組合. |
Draw | Canvas提供的繪製各種圖形方法。包括 :drawRect、drawLine、drawPath等等. |
drawRect
/** * 繪製矩形 * @param canvas * * 為什麼會有Rect和RectF兩種?兩者有什麼區別嗎? 答案當然是存在區別的,兩者最大的區別就是精度不同,Rect是int(整形)的,而RectF是float(單精度浮點型)的。 除了精度不同,兩種提供的方法也稍微存在差別,在這裡我們暫時無需關注 */ private void drawRect(Canvas canvas){ //one: canvas.drawRect(100,100,350,400,mPaint); //two: Rect rect = new Rect(100,100,350,400); canvas.drawRect(rect,mPaint); //three: RectF rectF = new RectF(100,100,350,400); canvas.drawRect(rectF,mPaint); }
drawLine
/** * 繪製線 * @param canvas */ private void drawLine(Canvas canvas){ canvas.drawLine(300,300,500,600,mPaint);// 在座標(300,300)(500,600)之間繪製一條直線 canvas.drawLines(new float[]{// 繪製一組線 每四數字(兩個點的座標)確定一條線 100,200,200,200, 100,300,200,300 },mPaint); }
drawPath
mPaint.setColor(Color.BLACK);// 畫筆顏色 - 黑色 mPaint.setStyle(Paint.Style.STROKE);// 填充模式 - 描邊 mPaint.setStrokeWidth(10);// 邊框寬度 - 10 canvas.translate(getWidth()/2,getHeight()/2); Path path = new Path(); path.lineTo(200,200);//預設從(0,0)開始 path.lineTo(200,0);//根據第一次移動完的位置設定 canvas.drawPath(path,mPaint);
以上只介紹了三個專案中使用的方法,其他暫時就不介紹了。這裡推薦 http://www.gcssloop.com/customview/Canvas_BasicGraphics 文章,可以更細緻的瞭解每一個方法使用,照著寫一遍就理解了。
四、動畫
知識點:ValueAnimator 、OvershootInterpolator
在 Android 動畫中,總共有兩種型別的動畫 View Animation(檢視動畫)和 Property Animator(屬性動畫);常見的View Animation 有 alpha、scale、translate、rotate 這裡不多介紹。
ValueAnimator
ValueAnimator 屬於 Property Animator(屬性動畫),ValueAnimator 本身不作用與任何物件,也就是說直接使用它是沒有任何動畫效果的,它是對指定值區間做動畫運算,通過監聽這個區間值的變化來修改控制元件的屬性值.
用法
第一步:建立 ValueAnimator
ValueAnimator animator = ValueAnimator.ofInt(0,100); animator.setDuration(1000); animator.start();
在這裡我們使用 ValueAnimator.ofInt 建立了一個值從 0 到 100 的動畫,動畫時長是 1s,然後讓動畫開始。可以看出,ValueAnimator 沒有跟任何的控制元件相關聯,那也正好說明 ValueAnimator 只是對值做動畫運算,而不是針對控制元件的,我們需要監聽 ValueAnimator 的動畫過程來自己對控制元件做操作。
第二步:新增監聽
ValueAnimator animator = ValueAnimator.ofInt(0,400); animator.setDuration(1000); animator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() { @Override public void onAnimationUpdate(ValueAnimator animation) { int curValue = (int)animation.getAnimatedValue(); } }); animator.start();
在上面的程式碼中,我們通過 addUpdateListener 新增一個監聽,在監聽傳回的結果中,表示當前狀態的 ValueAnimator 例項,通過 animation.getAnimatedValue() 得到當前值。
OvershootInterpolator
系統提供的回彈效果插值器,還有其他的效果,例如:BounceInterpolator(彈跳插值器)等等
配合ValueAnimator使用
ValueAnimator animator = ValueAnimator.ofInt(0,400); animator.setInterpolator(new OvershootInterpolator());
動畫效果實現思路
這裡只是根據程式碼過程簡單描述下實現思路
private ValueAnimator mValueAnimator = ValueAnimator.ofObject(new PointEvaluator(), mLastP, mCurrentP); private OvershootInterpolator mInterpolator = new OvershootInterpolator(1.5f); mValueAnimator.addUpdateListener(this); @Override public void onAnimationUpdate(ValueAnimator animation) { //會一直不停的回撥,知道到達 mCurrentP 位置停止 View currentTabView = mTabsContainer.getChildAt(this.mCurrentTab); IndicatorPoint p = (IndicatorPoint) animation.getAnimatedValue(); mIndicatorRect.left = (int) p.left; mIndicatorRect.right = (int) p.right; //這裡用於計算指示器的位置程式碼省略 invalidate(); } //每一次動畫更新就會之行 onDraw 方法,mIndicatorRect 中的 left 和 right 是根據動畫回撥變化的,所以通過重新整理就能看到指示器切換效果。 @Override protected void onDraw(Canvas canvas) { super.onDraw(canvas); // 繪製指示器,省略計算過程,詳細請檢視具體控制元件功能程式碼 mIndicatorDrawable.setBounds(left,top,right,bottom); }
注意事項
1、記憶體洩露。
2、setVisibility 失效。當多次頻繁對View 做動畫時,可能會出現無法顯示或隱藏問題,經除錯發現,需要使用View 的 clearAnimation()方法就能解決這個問題。
3、使用dp。由於Android 裝置解析度較多,使用dp比px要好。
五、資源屬性
本節會介紹Android 裝置顯示相關知識點,DisplayMetrics(density、scaledDensity)、sp、dp
關於DisplayMetrics 學習我們看官方文件瞭解就可以了,掌握含義和如何使用即可。
DisplayMetrics.java 是在android.util 包下,屬於工具類相關類,工具類的作用可以直接使用,直接使用結果。
我們可以看原始碼,或者官方文件解釋
A structure describing general information about a display, suchas its size, density, and font scaling. To access the DisplayMetrics members, initialize an object like this: DisplayMetrics metrics = new DisplayMetrics(); getWindowManager().getDefaultDisplay().getMetrics(metrics);
描述有關裝置顯示一般資訊結構,例如:大小、密度、字型縮放,可以通過以下方式獲取 DisplayMetrics 物件。
以上是官方文件簡單理解,按照上面的程式碼使用一點問題都沒有。
不過日常我們習慣這麼用
DisplayMetrics displayMetrics = context.getResources().getDisplayMetrics();
常見的欄位使用:
方法 | 含義 |
---|---|
scaledDensity | A scaling factor for fonts displayed on the display.字型縮放比例 |
density | The logical density of the display.螢幕密度 |
widthPixels | The absolute width of the available display size in pixels.螢幕寬度 |
heightPixels | The absolute height of the available display size in pixels.螢幕高度 |
其他欄位就不在這裡一一介紹了,如果有其他場景使用,可以檢視DisplayMetrics 完成程式碼。
說到和尺寸相關的能說好幾天,這裡就簡單介紹下概念,之後會總結下螢幕適配相關知識點。
px:即畫素,1px代表螢幕上一個物理的畫素點;
dp:在定義 UI 佈局時應使用的虛擬畫素單位,用於以密度無關方式表示佈局維度 或位置。
密度無關畫素等於 160 dpi 螢幕上的一個物理畫素,這是 系統為“中”密度螢幕假設的基線密度。在執行時,系統 根據使用中螢幕的實際密度按需要以透明方式處理 dp 單位的任何縮放 。
dp 單位轉換為螢幕畫素很簡單: px = dp * (dpi / 160)。 例如,在 240 dpi 螢幕上,1 dp 等於 1.5 物理畫素。在定義應用的 UI 時應始終使用 dp 單位 ,以確保在不同密度的螢幕上正常顯示 UI。
sp:縮放獨立的畫素,同dp相似,但是會根據系統字型大小偏好來縮放
對應關係
型別 | dpi | density |
---|---|---|
ldpi | 120 | 0.75 |
mdpi | 160 | 1.0 |
hdpi | 240 | 1.5 |
xhdpi | 320 | 2.0 |
等等
這裡要掌握,px和dp轉換.
protected int dp2px(float dp) { final float scale = mContext.getResources().getDisplayMetrics().density; return (int) (dp * scale + 0.5f); } protected int sp2px(float sp) { final float scale = this.mContext.getResources().getDisplayMetrics().scaledDensity; return (int) (sp * scale + 0.5f); }
六、Drawable
Drawable 簡介
Drawable 表示一種影象概念,可以是圖片也可以是其他顏色構造出得效果。實際開發中 Drawable 常被用來作為 View 的背景,一般都是通過 XML 檔案定義,但是通過程式碼也是可以的,這樣對於減小 APK 包體積大小也是有幫助的。
本節會介紹 Drawable 的兩個子類,這兩個是在專案中使用到的 Drawable, 分別是 GradientDrawable 和 StateListDrawable,我們先了解和掌握本庫是如何使用,其他子類目前暫時不介紹。
GradientDrawable
GradientDrawable 表示一個漸變區域,可以實現線性漸變、發散漸變和平鋪漸變效果(RECTANGLE, OVAL, LINE, RING)。GradientDrawable 是 Drawable 其中一個子類,用來設定按鈕背景、顏色、角度、邊框等,可以使用程式碼或者xml實現。
介紹幾個 Api:
方法 | 含義 |
---|---|
void setCornerRadius(float radius) | Specifies the radius for the corners of the gradient.指定四個角的圓角大小 |
void setCornerRadii(float[] radii) | Specifies radii for each of the 4 corners.分別指定4個角的圓角圓角半徑大小 |
void setColor (ColorStateList colorStateList) | Changes this drawable to use a single color state list instead of a gradient. Calling this method with a null argument will clear the color and is equivalent to calling setColor(int) with the argument TRANSPARENT.根據當前狀態設定顏色顯示. |
void setColor (int argb) | Changes this drawable to use a single color instead of a gradient.設定單色值. |
int[] getState () | Describes the current state, as a union of primitve states, such as state_focused, state_selected, etc. Some drawables may modify their imagery based on the selected state.返回當前選中狀態. |
int getShape () | Returns the type of shape used by this drawable, one of LINE, OVAL, RECTANGLE or RING.返回當前shape型別. |
public void setBounds (int left, int top, int right, int bottom) | 父類中的方法,用來指定繪製區域邊界 |
GradientDrawable 只是 Drawable 中其中一個子類,它還有很多的兄弟型別,用於處理不同的 Drawable 效果。我這裡先不介紹其他子類的情況,之後會總結下 Drawable 的所有子類,詳細介紹下每個子類功能。
2、StateListDrawable
StateListDrawable 表示一個多狀態對應不同 drawable 的 Drawable資源集合。是 Drawable 其中一個子類,常應用於 <selector> 實現各種按鈕點選態、預設態等功能。
介紹幾個 Api:
方法 | 含義 |
---|---|
void addState (int[] stateSet, Drawable drawable) | Add a new image/string ID to the set of images. 新增資源到一中狀態下 |
void applyTheme(Resources.Theme theme) | Applies the specified theme to this Drawable and its children.應用指定的主題到Drawable及其子項。 |
void inflate(Resources r, XmlPullParser parser, AttributeSet attrs, Resources.Theme theme) | Inflate this Drawable from an XML resource optionally styled by a theme.根據 XML 資源生成 Drawable。 |
boolean isStateful() | Indicates whether this drawable will change its appearance based on state.是否可以根據狀態改變當前顯示 |
Drawable mutate() | Make this drawable mutable.Drawable 可變,也就是可以生成一個新的變化樣式的Drawable |
StateSet:這些常量基本覆蓋了 Android 控制元件中大多數狀態,比如我們最常見的 PRESSED 狀態。通過位運算我們可以用一個 int 表示這裡所有的狀態。
StateSet
StateSet 在utils包下,屬於工具類輔助使用,簡單看下幾中狀態有個印象即可。
static final int[] VIEW_STATE_IDS = new int[] { R.attr.state_window_focused,VIEW_STATE_WINDOW_FOCUSED, R.attr.state_selected,VIEW_STATE_SELECTED, R.attr.state_focused,VIEW_STATE_FOCUSED, R.attr.state_enabled,VIEW_STATE_ENABLED, R.attr.state_pressed,VIEW_STATE_PRESSED, R.attr.state_activated,VIEW_STATE_ACTIVATED, R.attr.state_accelerated,VIEW_STATE_ACCELERATED, R.attr.state_hovered,VIEW_STATE_HOVERED, R.attr.state_drag_can_accept,VIEW_STATE_DRAG_CAN_ACCEPT, R.attr.state_drag_hovered,VIEW_STATE_DRAG_HOVERED };
應用
同樣一張圖需要三種不同狀態效果,例如:播放音樂下一首按鈕,三種狀態:不可用、預設、點選態。我們就能使用一張圖片,和三種著色搞定。
HintImageView.java StateListDrawable 實現三種狀態
七、完整Demo
https://github.com/whiskeyfei/FlycoTabLayout
八、擴充套件思路
出發點:原控制元件設計時 View 和資料都在一個類中,耦合性太強。
思考:控制元件只負責View相關,資料交給 Adapter,借鑑 RecyclerView 一些使用思路。
落地:控制元件只包含View 效果等實現,具體資料、顏色等一切可以設定的東西通過 Adapter 實現。
好處:通過切換 Adapter 實現不同效果。
壞處:沒有發揮自定義屬性優勢。
總結:使用 Adapter 形式有利有弊,目前還是初步階段,由於控制元件功能比較獨立,不像 RecyclerView 時專門做列表的,Adpater 設計沒有那麼靈活,後面會深入瞭解下 RecyclerView.Adpater 設計,借鑑一下。
demo 請檢視這裡
TabAdapter.java學習討論
剛剛建了一個 Android 開源庫分享學習群,有興趣的小夥伴可以加入一起學習。

群二維碼
參考資料
https://developer.android.com/reference/android/content/res/TypedArray
https://developer.android.com/reference/android/util/AttributeSet
https://developer.android.com/training/custom-views/create-view
https://developer.android.com/guide/topics/ui/custom-components
https://developer.android.com/training/custom-views/create-view
https://developer.android.com/reference/android/graphics/Rect
https://developer.android.com/reference/android/graphics/Paint
https://developer.android.com/reference/android/graphics/Canvas
http://www.gcssloop.com/customview/Canvas_BasicGraphics
https://developer.android.com/reference/android/graphics/drawable/GradientDrawable
https://developer.android.com/reference/android/graphics/drawable/StateListDrawable
https://developer.android.com/reference/android/animation/ValueAnimator
https://developer.android.com/reference/android/view/animation/OvershootInterpolator
http://wiki.jikexueyuan.com/project/android-animation/4.html
http://wiki.jikexueyuan.com/project/android-animation/5.html
https://developer.android.com/guide/practices/screens_support?hl=zh-cn