前言
上一篇文章講了如何 從ViewPager的源碼入手,定義自己的ViewPager滑動(dòng)特效。ViewPager由于有自己的動(dòng)畫接口接口,我們可以直接拿到當(dāng)前ItemView,以及它的position位置參數(shù),因此可以做出任何我們能夠想到的特效。
但是,TabLayout,谷歌貌似就沒有那么周到的服務(wù),像是今日頭條那樣的TabLayout 滑動(dòng)時(shí)的 文字部分顏色變化,還有很多其他app中出現(xiàn)的下方橫條indicator長(zhǎng)短變化的 特效,還有更多其他特效。如果使用谷歌原生的TabLayout是無(wú)法做到的,這個(gè)時(shí)候就需要我們 自定義TabLayout,但是,說(shuō)是自定義,前提還是要參照 谷歌的TabLayout源碼,然后在其基礎(chǔ)上進(jìn)行再創(chuàng)作。因?yàn)椋瑥?開始要制作一個(gè) 和谷歌原生同樣質(zhì)量的控件,包括滑動(dòng)流暢度和邊界控制,并且具有良好的擴(kuò)展性,并沒有那么容易,如果存在改造原生TabLayout的可能性,改造的代價(jià)要小于 從0創(chuàng)造。所以,優(yōu)先 閱讀源碼,探尋這種可能性,如果沒有可能性,再去從0創(chuàng)造。
Demo的地址為:https://github.com/18598925736/StudyTabLayout/tree/hank_v1
正文大綱
- 源碼分析
- 開發(fā)思路
- 開始搬磚
- 一. 尊重原著
- 二. 聯(lián)動(dòng)滑動(dòng)
- 三.特效解耦
正文
源碼分析
創(chuàng)建一個(gè)androidStudio工程,然后寫一個(gè)TabLayout+ViewPager效果,之后進(jìn)入看TabLayout的java源碼。源碼總長(zhǎng)度超過(guò)了3000行。但是其中大部分都是注釋以及空行,無(wú)需害怕。
從我們對(duì)TabLayout UI 上的感官印象,可以得知,它應(yīng)該是一個(gè)橫向可滾動(dòng)的 HorizontalScrollView, 它包含兩個(gè)關(guān)鍵的元素,一個(gè)是文字部分的 TabView,一個(gè)是 下劃線 Indicator .
image-20200320164934056-1585550575830.png
我們的動(dòng)畫也是圍繞這兩個(gè)部分,所以明確這次源碼分析的最終目標(biāo):
- TabView 在TabLayout中是如何 添加 進(jìn)去的
2)Indicator在TabLayout中是如何 繪制 進(jìn)去的
TabView明顯是一個(gè)以TextView為基礎(chǔ),所以是添加到TabLayout 中;Indicator是 一個(gè)圖形,所以應(yīng)該是繪制的。
帶上目標(biāo)來(lái)探索源碼,事半功倍。
開工。先看TabView.
從注釋中得知,TabLayout其實(shí)可以主動(dòng)去addTab來(lái)添加子view,那就從 addTab方法來(lái)入手。
image-20200320165636066-1585550578377.png
注釋中有這么一句。
tabLayout.addTab(tabLayout.newTab().setText("Tab 1"));
找到一個(gè)關(guān)鍵方法,關(guān)于Tab是如何創(chuàng)建的。
public Tab newTab() {
Tab tab = createTabFromPool();
tab.parent = this;
tab.view = createTabView(tab);
return tab;
}
從pool中創(chuàng)建,并且 指定了當(dāng)前TabLayout為它的parent,并且createTabView(tab)然后 給他的view屬性賦值。
后續(xù)還有一句:setText("Tab 1") , 在創(chuàng)建出來(lái)的Tab上調(diào)用setText方法,進(jìn)入看看:
public Tab setText(@Nullable CharSequence text) {
if (TextUtils.isEmpty(contentDesc) && !TextUtils.isEmpty(text)) {
// If no content description has been set, use the text as the content description of the
// TabView. If the text is null, don't update the content description.
view.setContentDescription(text);
}
this.text = text;
updateView();
return this;
}
由此得知,我們?cè)谏蠄D中所看到的title0 文本內(nèi)容,就是傳到了這個(gè)方法(可以debug驗(yàn)證)。那么 這里的 text 屬性用在了什么位置呢?內(nèi)部類TabView的updateTextAndIcon()方法
image-20200320173530548-1585550586641.png
從這段中可以得出,我們的文本內(nèi)容,最終傳給了 參數(shù) textView : textView.setText(text);
追蹤updateTextAndIcon()的調(diào)用位置,看看這個(gè)textView是什么。
經(jīng)過(guò)4處調(diào)用位置的檢查,
image-20200320174011680-1585550589010.png
發(fā)現(xiàn)它是:
image-20200320174106769-1585550590865.png
這兩個(gè)屬性之一。TabView的兩個(gè)TextView類型的成員變量。
那么只需要關(guān)心這兩個(gè)TextView是如何添加到TabView中去的。最后發(fā)現(xiàn) customTextView沒有被addView,而唯一一處addView(textView)的代碼如下:
private void inflateAndAddDefaultTextView() {
ViewGroup textViewParent = this;
if (BadgeUtils.USE_COMPAT_PARENT) {
textViewParent = createPreApi18BadgeAnchorRoot();
addView(textViewParent);
}
this.textView =
(TextView)
LayoutInflater.from(getContext())
.inflate(R.layout.design_layout_tab_text, textViewParent, false);
textViewParent.addView(textView);// 在這里添加的
}
所以,可以斷定,我們之前設(shè)置的 title0 這個(gè)文本被設(shè)置到 了 內(nèi)部類TabView(一個(gè)線性布局)的TextView成員中。
所以 title0 這個(gè)文本 的所在對(duì)象,從小到大,依次是,
原生TextView -> 內(nèi)部類TabView -> 內(nèi)部類Tab -> 原生TabLayout
基于這樣的認(rèn)知,要探究一下是不是存在文字被重新繪制的可能性。
要想對(duì)TextView根據(jù)需求重新繪制,那么除非可以像ViewPager一樣,把View以及當(dāng)前Position反饋到最外層。Position暫且不管,先看 最終的TextView.
經(jīng)過(guò)一番搏斗,發(fā)現(xiàn)并沒有這樣的接口。。。所以沒辦法了。TabView 在TabLayout中是如何 添加 進(jìn)去的 的探索結(jié)果表明,谷歌并沒有給機(jī)會(huì)讓我們 定制文本部分的內(nèi)容特效。所以,放棄吧。
然后從頭看起,如果以 Indicator在TabLayout中是如何 繪制 進(jìn)去的 為準(zhǔn)來(lái)進(jìn)行探索。
image-20200321201914158-1585550594385.png
這里有兩個(gè)方法,configureTab 方法,只是對(duì)tab對(duì)象進(jìn)行了 保存??碼ddTabView方法。
image-20200321202058037-1585550596434.png
這里的tab.view 是 TabView對(duì)象,它最終添加到了 slidingTabIndicator 中去。而 slidingTabIndicator 它則是一個(gè) 內(nèi)部類,同樣是線性布局,方向?yàn)闄M向,它把TabView對(duì)象添加進(jìn)去之后,多個(gè)TabView就會(huì)橫向排列。而底下那一個(gè)橫向的indicator,則是由 畫筆 selectedIndicatorPaint 繪制而成。根據(jù)如下:
image-20200321203148966-1585550597890.png
image-20200321203307642-1585550599417.png
得出最終結(jié)論:TabLayout的設(shè)計(jì)布局如下圖:
image-20200321204132776-1585550603705.png
最后我探索了一下,indicator 橫條,谷歌是不是有提供對(duì)外接口來(lái)編輯特效。倒是 內(nèi)部類 SlidingTabIndicator 有一個(gè)ValueAnimator indicatorAnimator 在控制 橫條滑動(dòng)的位置動(dòng)畫,使用的是 FastOutSlowInInterpolator 插值器。但是對(duì)我們自定義特效沒啥用。
最后結(jié)論,放棄治療了。在TabLayout上,谷歌確實(shí)不給機(jī)會(huì)。
開發(fā)思路
谷歌工程師設(shè)計(jì)的控件是針對(duì)全世界的開發(fā)者和使用者,肯定會(huì)考慮周全,支持很多自定義屬性,細(xì)節(jié)細(xì)致入微,所以代碼看上起會(huì)顯得非常復(fù)雜,難以讀懂,而且這么多英文注釋,你懂的,反正我看他們的注釋一邊看一邊猜。
然而我們的UI姐姐有自己的要求,所以如果我們可以做自己的UI控件,就可以擺脫谷歌源碼的控制,隨心所欲地控制TabLayout的視覺效果。
今天本文的最終目的:
是開發(fā)一個(gè) 綠色版的 GreenTabLayout,去掉谷歌原本一些繁雜的設(shè)定,增添開發(fā)常用的自定義屬性,并且開放 自定義效果的接口,讓其他開發(fā)者可以在不改動(dòng)我原本代碼的前提下,編輯自己的動(dòng)畫特效。
上面TabLayout UI層級(jí)圖,展示了谷歌工程師的設(shè)計(jì)思路,此思路沒有問題,我們可以參照它。
但是一步達(dá)成最終效果不太可能,我們分階段來(lái)達(dá)成效果:
-
尊重原著
GreenTabLayout 必須與原TabLayout相差不大,要有文字title標(biāo)題,要有indicator橫條
-
聯(lián)動(dòng)滑動(dòng)
自定義TabLayout必須能夠和ViewPager一樣,產(chǎn)生同樣的聯(lián)動(dòng)滑動(dòng)效果,包括橫條的滑動(dòng)和 標(biāo)題部分的滑動(dòng)
-
特效解耦
自定義TabLayout 把 標(biāo)題欄的View,indicator橫條View,對(duì)外提供方便的動(dòng)畫特效定制接口,符合開閉法則.
開始搬磚
確定了基本思路,接下來(lái)就要腳踏實(shí)地了。在Kotlin語(yǔ)言如此之香的潮流下,我也追求一波時(shí)尚,開發(fā)將采用Kotlin編碼,最大程度節(jié)省代碼量,使用kotlin"域"的概念隔離程序邏輯,盡可能使源碼可讀性提高。
一. 尊重原著
要實(shí)現(xiàn)與原生TabLayout一樣的效果,可以抄谷歌的作業(yè), 原本的UI層級(jí),照搬即可。
下載源碼之后,git checkout 4ed2 運(yùn)行看效果
從外到內(nèi)有三層:
最外層
它的最外層是一個(gè)橫向可滾動(dòng)的 HorizontalScrollView 的子類,同時(shí)它提供addTabView方法 供外界添加item
/**
* 最外層
*/
class HankTabLayout : HorizontalScrollView {
constructor(ctx: Context) : super(ctx) {
init()
}
constructor(ctx: Context, attributes: AttributeSet) : super(ctx, attributes) {
init()
}
constructor(context: Context, attrs: AttributeSet?, defStyleAttr: Int) : super(context, attrs, defStyleAttr) {
init()
}
private lateinit var indicatorLayout: IndicatorLayout
private fun init() {
indicatorLayout = IndicatorLayout(context)
val layoutParams = LinearLayout.LayoutParams(WRAP_CONTENT, MATCH_PARENT)
addView(indicatorLayout, layoutParams)
overScrollMode = View.OVER_SCROLL_NEVER
isHorizontalScrollBarEnabled = false
}
fun addTabView(text: String) {
indicatorLayout.addTabView(text)
}
}
中間層
中間層,是一個(gè)橫向線性布局,寬度自適應(yīng),根據(jù)內(nèi)容而定,提供addTabView方法,用來(lái)添加 TabView到自身,同時(shí) 繪制 indicator橫條, 橫條與當(dāng)前選中的tabView等長(zhǎng)并處于最下方.
繪制橫條可能有多種方式,這里借鑒了谷歌的思路,使用Drawable.draw(canvas) ,好處就是,可以指定drawable圖片,使用圖片內(nèi)容繪制在canvas上。后續(xù)會(huì)有體現(xiàn)。
/**
* 中間層 可滾動(dòng)的
*/
class IndicatorLayout : LinearLayout {
constructor(ctx: Context) : super(ctx) {
init()
}
private fun init() {
setWillNotDraw(false) // 如果不這么做,它自身的draw方法就不會(huì)調(diào)用
}
var indicatorLeft = 0
var indicatorRight = 0
/**
* 作為一個(gè)viewGroup,有可能它不會(huì)執(zhí)行自身的draw方法,這里有一個(gè)值去控制,好像是 setWillNotDraw
*/
override fun draw(canvas: Canvas?) {
val indicatorHeight = dpToPx(context, 4f)// 指示器高度
// 現(xiàn)在貌似應(yīng)該去畫indicator了
// 要繪制,首先要確定范圍,左上右下
var top = height - indicatorHeight
var bottom = height
Log.d("drawTag", "$indicatorLeft $indicatorRight $top $bottom")
// 現(xiàn)在只考慮在底下的情況
var selectedIndicator: Drawable = GradientDrawable()
selectedIndicator.setBounds(indicatorLeft, top, indicatorRight, bottom)
DrawableCompat.setTint(selectedIndicator, resources.getColor(R.color.c2))
selectedIndicator.draw(canvas!!)
super.draw(canvas)
}
fun updateIndicatorPosition(tabView: TabView, left: Int, right: Int) {
indicatorLeft = left
indicatorRight = right
postInvalidate()// 刷新自身,調(diào)用draw
// 把其他的都設(shè)置成未選中狀態(tài)
for (i in 0 until childCount) {
val current = getChildAt(i) as TabView
if (current.hashCode() == tabView.hashCode()) {// 如果是當(dāng)前被點(diǎn)擊的這個(gè),那么就不需要管
current.setSelectedStatus(true) // 選中狀態(tài)
} else {// 如果不是
current.setSelectedStatus(false)// 非選中狀態(tài)
}
}
}
/**
* 但是onDraw一定會(huì)執(zhí)行
*/
override fun onDraw(canvas: Canvas?) {
super.onDraw(canvas)
}
// 對(duì)外提供方法,添加TabView
fun addTabView(text: String) {
val tabView = TabView(context, this)
val param = LayoutParams(WRAP_CONTENT, WRAP_CONTENT)
param.setMargins(dpToPx(context, 10f))
val textView = TextView(context)
textView.setBackgroundDrawable(resources.getDrawable(R.drawable.my_tablayout_textview_bg))
textView.text = text
textView.gravity = Gravity.CENTER
textView.setPadding(dpToPx(context, 15f))
textView.setTextSize(TypedValue.COMPLEX_UNIT_SP, 20f)
textView.setTextColor(resources.getColor(TabView.unselectedTextColor))
tabView.setTextView(textView)
addView(tabView, param)
postInvalidate()
if (childCount == 1) {
val tabView0 = getChildAt(0) as TabView
tabView0.performClick()
}
}
}
最里層
說(shuō)是最里層,其實(shí)這里分為兩小層,一個(gè)是TabView(繼承線性布局),一個(gè)是TextView(用來(lái)展示 title),
提供點(diǎn)擊事件,和狀態(tài)切換的方法setSelectedStatus(boolean)
/**
* 最里層TabView
*/
class TabView : LinearLayout {
private lateinit var titleTextView: TextView
private var selectedStatue: Boolean = false
private var parent: IndicatorLayout
companion object {
const val selectedTextColor = R.color.cf
const val unselectedTextColor = R.color.c1
}
constructor(ctx: Context, parent: IndicatorLayout) : super(ctx) {
init()
this.parent = parent
}
fun setTextView(textView: TextView) {
titleTextView = textView
removeAllViews()
val param = LayoutParams(WRAP_CONTENT, MATCH_PARENT)
addView(titleTextView, param)
titleTextView.setOnClickListener {
parent.updateIndicatorPosition(this, left, right)
}
}
private fun init() {
}
fun setSelectedStatus(selected: Boolean) {
selectedStatue = selected
if (selected) {
titleTextView.setTextColor(resources.getColor(R.color.cf))
} else {
titleTextView.setTextColor(resources.getColor(R.color.c1))
}
}
}
初階效果
做完這些,基本就呈現(xiàn)出下圖的狀態(tài):
尊重原著.gif
上一半是原生TabLayout,用來(lái)對(duì)比,下一半是剛剛完成的效果。但是和上面的原生TabLayout比起來(lái). 第一步完成。從開始寫代碼,到完成這個(gè)效果,一直參考的 谷歌的代碼。
二. 聯(lián)動(dòng)滑動(dòng)
下載源碼之后,git checkout a132 運(yùn)行看效果
布局層級(jí)已經(jīng)完成,現(xiàn)在需要聯(lián)動(dòng)Viewpager的滑動(dòng)參數(shù),讓GreenTabLayout 跟隨ViewPager一起滑動(dòng)。
注冊(cè)監(jiān)聽
要實(shí)現(xiàn)聯(lián)動(dòng),首先要知道,谷歌源碼中,TabLayout是如何與ViewPager發(fā)生聯(lián)動(dòng)的,它們的聯(lián)結(jié)點(diǎn)在哪里,請(qǐng)看代碼:
tabLayout.setupWithViewPager(viewpager)
平時(shí)我們用 原生TabLayout,兩者唯一發(fā)生交集的地方就是這里,進(jìn)入看源碼:
image-20200330142618611.png
顯然他們的交集可能是某個(gè)回調(diào)監(jiān)聽,順著這個(gè)線索,最終確定,上面的 pageChangeListener就是 聯(lián)動(dòng)滑動(dòng)的交界點(diǎn),這里把監(jiān)聽器傳給ViewPager,ViewPager則可以把自己的滑動(dòng)參數(shù)傳遞給TabLayout,TabLayout則做出相應(yīng)的行為。
監(jiān)聽器的源碼為:
private TabLayoutOnPageChangeListener pageChangeListener;
public static class TabLayoutOnPageChangeListener implements ViewPager.OnPageChangeListener {
@Override
public void onPageScrolled(final int position, final float positionOffset, final int positionOffsetPixels) {
....
}
@Override
public void onPageSelected(final int position) {
...
}
@Override
public void onPageScrollStateChanged(final int state) {
...
}
}
了解到這里,我們可以給 GreenTabLayuot 直接加上 這個(gè)接口實(shí)現(xiàn)
class GreenTabLayout : HorizontalScrollView, ViewPager.OnPageChangeListener {
@Override
public void onPageScrolled(final int position, final float positionOffset, final int positionOffsetPixels) {
....
}
@Override
public void onPageSelected(final int position) {
...
}
@Override
public void onPageScrollStateChanged(final int state) {
...
}
}
然后提供一個(gè) 相同的 setupWithViewPager(viewpager) 方法, 在內(nèi)部,給ViewPager綁定監(jiān)聽,同時(shí)根據(jù) viewPager的adapter內(nèi)的 page數(shù)目,決定TabView的數(shù)目和每一個(gè)的標(biāo)題。
fun setupWithViewPager(viewPager: ViewPager) {
this.mViewPager = viewPager
viewPager.addOnPageChangeListener(this)// 注冊(cè)監(jiān)聽
val adapter = viewPager.adapter ?: return
val count = adapter!!.count // 欄目數(shù)量
for (i in 0 until count) {
val pageTitle = adapter.getPageTitle(i)
addTabView(pageTitle.toString())// 根據(jù)adapter的item數(shù)目,決定TabView的數(shù)目和每一個(gè)標(biāo)題
}
}
參數(shù)分析
注冊(cè)監(jiān)聽之后,Viewpager可以把自己的滑動(dòng)參數(shù)的變化告知TabLayout,但是TabLayout如何去處理這個(gè)參數(shù)變化,還需要從參數(shù)的規(guī)律上去著手。重點(diǎn)分析 監(jiān)聽的 onPageScrolled 方法, 重點(diǎn)中的重點(diǎn),則是前兩個(gè)參數(shù):position(當(dāng)前page的index) 和 positionOffset(當(dāng)前page的偏移百分比,小數(shù)表示的)
為了研究規(guī)律,我們用上面剛剛完成的代碼把GreenTabLayout和ViewPager連結(jié)上,然后打印日志onPageScrolled :
image-20200330145008704.png
基本得出一個(gè)結(jié)論:
position為0的,為當(dāng)前選中的這個(gè)page,當(dāng)慢慢從當(dāng)前page劃走時(shí),它的positionOffset會(huì)從0慢慢變成1
并且,如果手指分方向滑動(dòng)試驗(yàn),可知:
當(dāng)手指向左,positionOffset會(huì)遞增,從0到極限值1,到達(dá)極限之后歸0,同時(shí) position遞加1
反之,手指向右,positionOffset會(huì)遞減,從1 遞減到0,從遞減的那一刻開始,position遞減1
基于上面的規(guī)律,我們可以調(diào)試出 indicator橫條動(dòng)畫的代碼:
...
override fun onPageScrolled(position: Int, positionOffset: Float, positionOffsetPixels: Int) {
scrollTabLayout(position, positionOffset)
}
private fun scrollTabLayout(position: Int, positionOffset: Float) {
// 如果手指向左劃,indicator橫條應(yīng)該從當(dāng)前位置,滑動(dòng)到 下一個(gè)子view的位置上去,position應(yīng)該+1
// 如果手指向右滑動(dòng),position立即減1,indicator橫條應(yīng)該從當(dāng)前位置向左滑動(dòng)
val currentTabView = indicatorLayout.getChildAt(position) as GreenTabView
val currentLeft = currentTabView.left
val currentRight = currentTabView.right
val nextTabView = indicatorLayout.getChildAt(position + 1)
if (nextTabView != null) {
val nextLeft = nextTabView.left
val nextRight = nextTabView.right
Log.d("scrollTabLayout","當(dāng)前index:${position} left:${currentLeft} right:${currentRight} " +" 目標(biāo)index:${position + 1} left:${nextLeft} right:${nextRight} positionOffset:${positionOffset}" )
val leftDiff = nextLeft - currentLeft
val rightDiff = nextRight - currentRight
indicatorLayout.updateIndicatorPosition(
currentLeft + (leftDiff * positionOffset).toInt(),
currentRight + (rightDiff * positionOffset).toInt()
)
}
}
為什么這樣就能正確區(qū)分滑動(dòng)的方向?把日志打印出來(lái)一看就明白:
這是手指向左劃一格:
image-20200330151551105.png
觀察positionOffset的變化,從0 變?yōu)?,然后歸零。
而看橫條的當(dāng)前 left = 26,right=170, 以及 目標(biāo)left=222,right=380 ,隨著positionOffset的遞增,橫條會(huì)慢慢向右。
而到達(dá)最后,positionOffset歸零了,當(dāng)前l(fā)eft 也變成了 目標(biāo)的left = 222,right=380.
橫條向右平移完成。
而手指向右劃一格,日志如下:
image-20200330152206881.png
position先直接減1,positionOffset則從1慢慢變成0.
橫條從 left=26 right=170 的起始位置,向 目標(biāo) left=222,righ=380 移動(dòng),但是由于positionOffset是遞減的,所以,橫條的移動(dòng)方向反而是 向左。一直到positionOffset為0,到達(dá) left=26 right=170.
橫條向左平移也完成。
整體平移
橫條雖然可以跟著viewPager的滑動(dòng)而滑動(dòng),但是如果TabView已經(jīng)排滿了當(dāng)前屏幕,橫條到達(dá)了當(dāng)前屏幕最右側(cè),viewPager上右側(cè)還有內(nèi)容還可以讓手指向左滑動(dòng)。此時(shí),就必須滾動(dòng)最外層布局,來(lái)讓TabView顯示出來(lái)。
通過(guò)觀察原生TabLayout,它會(huì)盡量讓 當(dāng)前選中的tabView位于 控件的橫向居中的位置。而隨著 ViewPager的當(dāng)前page的變化,最外層GreenTabLayout也要發(fā)生橫向滾動(dòng)。
所以我選擇在 回調(diào)函數(shù)onPageSelected中執(zhí)行滾動(dòng):
class GreenTabLayout: HorizontalScrollView, ViewPager.OnPageChangeListener {
...
override fun onPageSelected(position: Int) {
val tabView = indicatorLayout.getChildAt(position) as GreenTabView
if (tabView != null) {
indicatorLayout.updateIndicatorPositionByAnimator(tabView, tabView.left, tabView.right)
}
}
}
執(zhí)行滾動(dòng)的思路為:
- 確定 當(dāng)前選中的tabView的 矩形范圍
tabView.getHitRect(tabViewBounds) - 確定 確定最外層GreenTbaLayout的矩形范圍
getHitRect(parentBounds) - 計(jì)算兩個(gè)矩形的x軸的中點(diǎn),然后計(jì)算出兩個(gè)中點(diǎn)的差值,差值就是需要滾動(dòng)的距離
- 使用屬性動(dòng)畫進(jìn)行平滑滾動(dòng)
/**
* 用動(dòng)畫平滑更新indicator的位置
* @param tabView 當(dāng)前這個(gè)子view
*/
fun updateIndicatorPositionByAnimator(
tabView: GreenTabView,
targetLeft: Int,
targetRight: Int) {
...
// 處理最外層布局( HankTabLayout )的滑動(dòng)
parent.run {
tabView.getHitRect(tabViewBounds) //確定 當(dāng)前選中的tabView的 矩形范圍
getHitRect(parentBounds) // 確定最外層GreenTbaLayout的矩形范圍
val scrolledX = scrollX // 已經(jīng)滑動(dòng)過(guò)的距離
val tabViewRealLeft = tabViewBounds.left - scrolledX // 真正的left, 要算上scrolledX
val tabViewRealRight = tabViewBounds.right - scrolledX // 真正的right, 要算上scrolledX
val tabViewCenterX = (tabViewRealLeft + tabViewRealRight) / 2
val parentCenterX = (parentBounds.left + parentBounds.right) / 2
val needToScrollX = -parentCenterX + tabViewCenterX // 差值就是需要滾動(dòng)的距離
startScrollAnimator(this, scrolledX, scrolledX + needToScrollX)
}
}
/**
* 用動(dòng)畫效果平滑滾動(dòng)過(guò)去
*/
private fun startScrollAnimator(tabLayout: GreenTabLayout, from: Int, to: Int) {
if (scrollAnimator != null && scrollAnimator.isRunning) scrollAnimator.cancel()
scrollAnimator.duration = 200
scrollAnimator.interpolator = FastOutSlowInInterpolator()
scrollAnimator.addUpdateListener {
val progress = it.animatedValue as Float
val diff = to - from
val currentDif = (diff * progress).toInt()
tabLayout.scrollTo(from + currentDif, 0)
}
scrollAnimator.start()
}
二階效果
完成到這里,就能達(dá)成下圖中的效果:
聯(lián)動(dòng)滑動(dòng).gif
上半部分為原生TabLayout效果,下把那部分為 剛剛完成的效果,幾乎沒有差別了。
當(dāng)然,我們這是把TabLayout本地化,完成這些,僅僅用了kotlin 300多行代碼??梢奒otlin在省代碼方面,確實(shí)是一絕,比java簡(jiǎn)潔很多。
三.特效解耦
這一階段主要做2件事:
- 支持開發(fā)中的常用的UI設(shè)計(jì)要求,這個(gè)可以做成自定義屬性
- 開放無(wú)耦合接口,使得開發(fā)者可以使用該接口編輯 indicator橫條 / TabView文本 的滑動(dòng)特效,而不用改動(dòng)GreenTabLayout的內(nèi)部實(shí)現(xiàn)
第一點(diǎn),都是一些基礎(chǔ)性的改造,就不贅述了,關(guān)于自定義屬性的添加和使用,都是死框架,沒什么好說(shuō)的,下面,總結(jié)一下 我所支持的所有屬性:
盤點(diǎn)自定義屬性
TabView標(biāo)題欄部分:
| 屬性名 | 意義 | 取值類型 |
|---|---|---|
| tabViewTextSize | 標(biāo)題字體大小 | dimension|reference |
| tabViewTextSizeSelected | 選中后的標(biāo)題字體大小 | dimension|reference |
| tabViewTextColor | 標(biāo)題字體顏色 | color|reference |
| tabViewTextColorSelected | 選中后的標(biāo)題字體顏色 | color|reference |
| tabViewBackgroundColor | 標(biāo)題區(qū)域背景色 | color|reference |
| tabViewTextPaddingLeft | 標(biāo)題區(qū)內(nèi)邊距左 | dimension|reference |
| tabViewTextPaddingRight | 標(biāo)題區(qū)內(nèi)邊距右 | dimension|reference |
| tabViewTextPaddingTop | 標(biāo)題區(qū)內(nèi)邊距上 | dimension|reference |
| tabViewTextPaddingBottom | 標(biāo)題區(qū)內(nèi)邊距下 | dimension|reference |
| tabViewDynamicSizeWhenScrolling | 是否允許滾動(dòng)時(shí)的字體大小漸變 | boolean |
Indicator橫條部分:
| 屬性名 | 意義 | 取值類型 |
|---|---|---|
| indicatorColor | 橫條顏色 | color|reference |
| indicatorLocationGravity | 橫條位置 | 枚舉:TOP 放在頂部 / BOTTOM 放在底部 |
| indicatorMargin | 橫條間距,當(dāng)indicatorLocationGravity為TOP時(shí)表示距離頂端的距離,BOTTOM時(shí)表示距離底部的距離 | dimension|reference |
| indicatorWidthMode | 橫條寬度模式 | 枚舉:RELATIVE_TAB_VIEW 取TabView寬度的倍數(shù) / EXACT 取精確值 |
| indicatorWidthPercentages | 橫條寬度百分比,當(dāng)indicatorWidthMode 為 RELATIVE_TAB_VIEW時(shí)才會(huì)生效,表示橫條寬度占TabView寬度的百分比 |
float(大于0) |
| indicatorExactWidth | 橫條寬度精確值,當(dāng)indicatorWidthMode 為 EXACT時(shí)才會(huì)生效,表示橫條的精確寬度 |
dimension|reference |
| indicatorHeight | 橫條高度 | dimension|reference |
| indicatorAlignMode | 橫條對(duì)其模式 | 枚舉: LEFT / CENTER / RIGHT |
| indicatorDrawable | 橫條drawable,可以指定橫條的內(nèi)容為圖片 | reference |
| indicatorElastic | 是否開啟滾動(dòng)時(shí)橫條的彈性效果 | boolean |
| indicatorElasticBaseMultiple | 當(dāng)indicatorElastic開啟時(shí)生效,表示彈性倍數(shù),數(shù)字越大,彈性越明顯 | float |
其中大部分屬性的處理都是基于非常基礎(chǔ)的View控件知識(shí)和簡(jiǎn)單的數(shù)學(xué)計(jì)算,只有幾點(diǎn)需要講解說(shuō)明:
- tabViewDynamicSizeWhenScrolling 是否允許滾動(dòng)時(shí)的字體大小漸變
- indicatorElastic 是否開啟滾動(dòng)時(shí)橫條的彈性效果
這兩點(diǎn),都與 viewPager滑動(dòng)時(shí)的參數(shù)變化有關(guān)系,所以處理這兩個(gè)特性,需要結(jié)合參數(shù)變化規(guī)律
較復(fù)雜屬性處理
-
tabViewDynamicSizeWhenScrollingviewPager滾動(dòng)時(shí),標(biāo)題的字體大小會(huì)發(fā)生漸變:
標(biāo)題欄字體大小漸變.gif
class GreenTabLayout : HorizontalScrollView, ViewPager.OnPageChangeListener {
...
override fun onPageScrolled(position: Int, positionOffset: Float, positionOffsetPixels: Int){
Log.d("positionOffset", "$positionOffset")
scrollTabLayout(position, positionOffset)
}
fun scrollTabLayout(position: Int, positionOffset: Float) {
val currentTabView = indicatorLayout.getChildAt(position) as GreenTabView
val currentLeft = currentTabView.left
val currentRight = currentTabView.right
val nextTabView = indicatorLayout.getChildAt(position + 1) // 目標(biāo)TabView
if (nextTabView != null) {
val nextGreenTabView = nextTabView as GreenTabView
dealAttrTabViewDynamicSizeWhenScrolling(// 關(guān)鍵代碼
positionOffset,
currentTabView,
nextGreenTabView
)
...
}
}
/**
* 處理屬性 tabViewDynamicSizeWhenScrolling
*/
private fun dealAttrTabViewDynamicSizeWhenScrolling(
positionOffset: Float,
currentTabView: GreenTabView,
nextTabView: GreenTabView
) {
if (tabViewAttrs.tabViewDynamicSizeWhenScrolling) {
if (positionOffset != 0f) {
// 在這里,讓當(dāng)前字體變小,next的字體變大
val diffSize =
tabViewAttrs.tabViewTextSizeSelected - tabViewAttrs.tabViewTextSize
when (mScrollState) {
ViewPager.SCROLL_STATE_DRAGGING -> {
currentTabViewTextSizeRealtime =
tabViewAttrs.tabViewTextSizeSelected - diffSize * positionOffset
currentTabView.titleTextView.setTextSize(
TypedValue.COMPLEX_UNIT_PX,
currentTabViewTextSizeRealtime
)
nextTabViewTextSizeRealtime =
tabViewAttrs.tabViewTextSize + diffSize * positionOffset
nextTabView.titleTextView.setTextSize(
TypedValue.COMPLEX_UNIT_PX,
nextTabViewTextSizeRealtime
)
settingFlag = false
}
ViewPager.SCROLL_STATE_SETTLING -> {
// OK,定位到問題,在 mScrollState 為setting狀態(tài)時(shí),positionOffset的變化沒有 dragging時(shí)那么細(xì)致
// 只要不處理 SETTING下的字體大小變化,也可以達(dá)成效果
if (!settingFlag)
indicatorLayout.resetTabViewsStatueByAnimator(indicatorLayout[mCurrentPosition] as GreenTabView)
settingFlag = true
}
}
}
}
}
}
處理思路依舊是圍繞 onPageScrolled 的參數(shù)變化,核心方法為:dealAttrTabViewDynamicSizeWhenScrolling(..), 讓當(dāng)前tabView的文本漸漸變小,而nextTabView的文本逐漸變大。這里如果有疑問可以參照上文的 參數(shù)分析小章節(jié)。
但是,有一個(gè)坑,就是當(dāng)拖拽停止的時(shí)候,viewpager會(huì)有一個(gè)自動(dòng)的回彈動(dòng)作,如果這里沒處理好,就會(huì)出現(xiàn),字體大小突變的情況,和我要的平滑動(dòng)畫過(guò)渡不相符,所以,這里我做了一個(gè)特殊處理,當(dāng)拖拽停止,也就是手指松開的時(shí)候,抓準(zhǔn) ViewPager的 SCROLL_STATE_SETTLING 狀態(tài)剛剛進(jìn)入的時(shí)機(jī),使用屬性動(dòng)畫平滑改變字體,核心代碼就是上文代碼塊中的:indicatorLayout.resetTabViewsStatueByAnimator(indicatorLayout[mCurrentPosition] as GreenTabView) 這句話可以讓 tabView的文本字體平滑地從 當(dāng)前值(不確定,因?yàn)閐ragging狀態(tài)是用戶人為控制),變?yōu)?目標(biāo)值(這是確定值,要么是 正常狀態(tài)下的字體大小,要么是選中狀態(tài)下的字體大?。?,由此完美解決字體平滑變化的問題。
-
indicatorElastic滾動(dòng)時(shí),橫條會(huì)拉伸和回縮,也是跟隨onPageScrolled的參數(shù)變化而變化關(guān)鍵代碼在
SlidingIndicatorLayout.kt中的 draw方法:override fun draw(canvas:Canvas?){ ... val baseMultiple = parent.indicatorAttrs.indicatorElasticBaseMultiple // 基礎(chǔ)倍數(shù),決定拉伸 val indicatorCriticalValue = 1 + baseMultiple val ratio = if (parent.indicatorAttrs.indicatorElastic) { when { positionOffset >= 0 && positionOffset < 0.5 -> { 1 + positionOffset * baseMultiple // 拉伸長(zhǎng)度 } else -> {// 如果到了下半段,當(dāng)offset越過(guò)中值之后ratio的值 indicatorCriticalValue - positionOffset * baseMultiple } } } else 1f // 可以開始繪制 selectedIndicator.run { setBounds( ((centerX - indicatorWidth * ratio / 2).toInt()), top, ((centerX + indicatorWidth * ratio / 2).toInt()), bottom )// 規(guī)定它的邊界 draw(canvas!!)// 然后繪制到畫布上 } ... }這一段提出來(lái)特別說(shuō)明,因?yàn)樗砹艘环N解題思路,我需要的效果是:
viewPager滾動(dòng)1格,我需要它在滾動(dòng)一半的時(shí)候,橫條拉伸到最長(zhǎng),從一半滾完的時(shí)候,橫條回縮到應(yīng)該的寬度
但是,viewPager滾1格,positionOffset的變化是從0 到1(手指向右),或者是從1到0(手指向左),我需要把positionOffset在到達(dá)0.5的時(shí)候當(dāng)作一個(gè)臨界時(shí)間點(diǎn),計(jì)算出 這個(gè)臨界時(shí)間點(diǎn)上,indicator橫條應(yīng)該的長(zhǎng)度。
關(guān)鍵在于:在臨界點(diǎn)0.5上,前半段的0->0.5的最終值,必須等于 后半段 0.5->1 的 開始值,
由于我是按照倍數(shù)來(lái)拉伸,所以,原始倍率是1。我還想用參數(shù)控制拉伸的程度,所以設(shè)計(jì)一個(gè)變量
baseMultiple(拉伸倍數(shù),數(shù)值越大,拉伸越明顯)列出公式:
前半段的ratio最終值 = 1(
原始倍率)+ 0.5 *baseMultiple后半段的ratio值 =
indicatorCriticalValue(臨界值) - 0.5 *baseMultiple前半段的ratio最終值 = 后半段的ratio值
計(jì)算得出,
indicatorCriticalValue(臨界值) = 1 (原始倍率)+baseMultiple于是就寫出了上面的代碼。
三階效果
說(shuō)了這么多,不如親眼看一眼效果更佳實(shí)在,以上各項(xiàng)屬性,下面的動(dòng)態(tài)圖基本都有體現(xiàn), 具體效果可以按需定制,基本可以滿足UI姐姐的各種騷操作要求,如果還不行,可以拿我的代碼自行修改,我的代碼注釋應(yīng)該比谷歌大佬要親民很多。,歡迎fork,star...

開放無(wú)耦合特效接口
為什么生出這種想法?這個(gè)是源自:ViewPager的無(wú)耦合動(dòng)畫接口。
Viewpager.setPageTransformer(true, MyPageTransformer(this, adapter.count))
viewPager的setPageTransformer,可以傳入一個(gè) PageTransformer(接口)的實(shí)現(xiàn)類,從而控制ViewPager滑動(dòng)時(shí)的動(dòng)畫,開發(fā)者可以自由定制效果,而不用關(guān)心ViewPager的內(nèi)部實(shí)現(xiàn)。符合程序設(shè)計(jì)的開閉法則,讓控件開發(fā)者和 控件使用者都省心省力。
GreenTabView接口
我在Demo中,提供了 GreenTabLayout的setupWithViewPager泛型方法,使用者可以傳入 GreenTextView的子類.兩段關(guān)鍵代碼如下:
open class GreenTextView : AppCompatTextView {
constructor(context: Context, attrs: AttributeSet) : super(context, attrs)
constructor(context: Context) : super(context)
/**
* 可重寫,接收來(lái)自viewpager的position參數(shù),做出隨心所欲的textView特效
*
* @param isSelected 是不是當(dāng)前選中的TabView
* @param positionOffset 偏移值 0<= positionOffset <=1
*/
open fun handlerPositionOffset(positionOffset: Float, isSelected: Boolean) {}
/**
* 如果發(fā)生了滑動(dòng)過(guò)程中特效殘留的情況,可以重寫此方法用來(lái)清除特效
*/
open fun removeShader(oldPosition: Int, newOldPosition: Int) {}
/**
* 添加特效
*/
open fun addShader(oldPosition: Int, newOldPosition: Int) {}
/**
* 通知,viewPager 即將進(jìn)入setting狀態(tài)
* @param positionOffset 當(dāng)前offset
* @param isSelected 是否是被選擇的TabView
* @param direction 滑動(dòng)方向,大于0 表示向右回彈,小于0 表示向左回彈
*/
open fun onSetting(positionOffset: Float, isSelected: Boolean, direction: Int) {}
}
class GreenTabLayout : HorizontalScrollView, ViewPager.OnPageChangeListener{
...
fun <T : GreenTextView> setupWithViewPager(viewPager: ViewPager, t: T?) {
...
}
}
你可以按照下面的模板使用這個(gè)接口:
class MainActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
val adapter = MyPagerAdapter(supportFragmentManager)
hankViewpager.adapter = adapter
hankViewpager.offscreenPageLimit = 3
hankViewpager.setPageTransformer(true, MyPageTransformer(this, adapter.count))
//*******************關(guān)鍵代碼*****************
hankTabLayout.setupWithViewPager(hankViewpager, GradientTextView(this))
//*******************************************
hankTabLayout2.setupWithViewPager(hankViewpager)
}
....
}
GradientTextView是GreenTabView的一個(gè)子類,它的源碼是:
/**
* 提供顏色漸變的TextView
*/
class GradientTextView : GreenTextView {
constructor(context: Context, attrs: AttributeSet) : super(context, attrs)
constructor(context: Context) : super(context)
private var mLinearGradient: LinearGradient? = null
private var mGradientMatrix: Matrix? = null
private lateinit var mPaint: Paint
private var mViewWidth = 0f
private var mTranslate = 0f
private val mAnimating = true
private val fontColor = Color.BLACK
private val shaderColor = Color.YELLOW
override fun onSizeChanged(w: Int, h: Int, oldw: Int, oldh: Int) {
super.onSizeChanged(w, h, oldw, oldh)
if (mViewWidth == 0f) {
mViewWidth = measuredWidth.toFloat()
if (mViewWidth > 0) {
mPaint = paint
mLinearGradient = LinearGradient(
0f,// 初始狀態(tài),是隱藏在x軸負(fù)向,一個(gè)view寬的距離
0f,
mViewWidth,
0f,
intArrayOf(fontColor, shaderColor, shaderColor, fontColor),
floatArrayOf(0f, 0.1f, 0.9f, 1f),
Shader.TileMode.CLAMP
)
mPaint.shader = mLinearGradient
mGradientMatrix = Matrix()
}
}
}
override fun onDraw(canvas: Canvas) {
super.onDraw(canvas)
if (mAnimating && mGradientMatrix != null) {
mGradientMatrix!!.setTranslate(mTranslate, 0f)
mLinearGradient!!.setLocalMatrix(mGradientMatrix)
}
}
private inline fun dealSwap(positionOffset: Float, isSelected: Boolean) {
// 如果不是初始值,那說(shuō)明已經(jīng)賦值過(guò),那么用 參數(shù)positionOffset 和 它對(duì)比,來(lái)得出滑動(dòng)的方向
Log.d(
"setMatrixTranslate",
" positionOffset:$positionOffset isSelected:$isSelected "
)
// 來(lái),先判定滑動(dòng)的方向,因?yàn)榉较驎?huì)決定從哪個(gè)角度
mTranslate = if (mPositionOffset < positionOffset) {// 手指向左
if (isSelected) {// 如果當(dāng)前是選中狀態(tài),那么 offset會(huì)從0到1 會(huì)如何變化?
mViewWidth * positionOffset // OK,沒問題。
} else {
-mViewWidth * (1 - positionOffset)
}
} else {// 手指向右
if (isSelected) {// 如果當(dāng)前是選中狀態(tài),那么 offset會(huì)從0到1 會(huì)如何變化?
-mViewWidth * (1 - positionOffset) // OK,沒問題。
} else {
mViewWidth * positionOffset
}
}
postInvalidate()
}
/**
* 由外部參數(shù)控制shader的位置
* @param positionOffset 只會(huì)從0到1變化
* @param isSelected 是否選中
*/
override fun handlerPositionOffset(positionOffset: Float, isSelected: Boolean) {
if (mPositionOffset == -1f) {// 如果你是初始值
mPositionOffset = positionOffset // 那就先賦值
} else {
dealSwap(positionOffset, isSelected)
}
}
override fun removeShader(direction: Int) {
Log.d("removeShaderTag", "要根據(jù)它當(dāng)前的mTranslate位置決定從哪個(gè)方向消失 mTranslate:$mTranslate")
mTranslate = mViewWidth
postInvalidate()
}
override fun addShader(direction: Int) {
// 屬性動(dòng)畫實(shí)現(xiàn)shader平滑移動(dòng)
val from =
if (direction < 0) {
-mViewWidth
} else {
mViewWidth
}
startAnimator(from, 0f)
}
override fun onSetting(positionOffset: Float, isSelected: Boolean, direction: Int) {
Log.d(
"onSettingTag",
"isSelected:$isSelected positionOffset:$positionOffset direction:$direction"
)
mPositionOffset = -1f
val targetTranslate = if (isSelected) {
0f
} else {
if (direction > 0f) {// 向右回彈
mViewWidth
} else {
Log.d("onSettingTag2", "難道這里還要分情況么?mTranslate:$mTranslate mViewWidth:$mViewWidth")
if (mTranslate == mViewWidth || mTranslate == -mViewWidth) {
mTranslate // 如果已經(jīng)到達(dá)了最右邊,那就保持你這個(gè)樣子就行了, 可是你是怎么到最右邊的?
} else
-mViewWidth
}
}
val thisTranslate = mTranslate
startAnimator(thisTranslate, targetTranslate)
}
private fun startAnimator(from: Float, targetTranslate: Float) {
if (animator != null) animator?.cancel()
// 屬性動(dòng)畫實(shí)現(xiàn)shader平滑移動(dòng)
animator = ValueAnimator.ofFloat(from, targetTranslate)
animator?.run {
duration = animatorDuration
addUpdateListener {
mTranslate = it.animatedValue as Float
postInvalidate()
}
start()
}
}
private var mPositionOffset: Float = -1f
private val animatorDuration = 200L
private var animator: ValueAnimator? = null
}
運(yùn)行效果:請(qǐng)注意看下圖的上面半部分,下半部分只是沒有加特效的對(duì)比。理論上,利用現(xiàn)在的參數(shù),可以定制出想要的任何效果,下圖只是我的一些效果測(cè)試。
文字漸變最終效果.gif
注意,使用了Shader特效之后,原本的 titleTextView字體顏色可能會(huì)失效,這是由shader機(jī)制決定的,但是依然可以用shader控制字體的顏色,運(yùn)行Demo,閱讀源碼,很快就能得出答案。
既然這是一個(gè)開放接口,那么所能達(dá)成的效果,就不僅僅是上圖中所示, 利用 handlerPositionOffset的幾個(gè)參數(shù),發(fā)揮想象力(或者UI姐姐發(fā)揮想象力),想要做出任何你希望的效果,只是時(shí)間問題。
Indicator接口
同樣,針對(duì)Indicator橫條的繪制,你也可以完全自定義,使用自己的實(shí)現(xiàn)方式,強(qiáng)制接管 原代碼中的繪制邏輯。
接口在 GreenTabLayout.kt 中,入口方法為:
/**
* 注意,使用了此方法,傳入了非空的CustomDrawHandler實(shí)現(xiàn)類對(duì)象,
* 原本indicator的所有屬性都會(huì)失效,因?yàn)閕ndicator的繪制工作,全部由CustomDrawHandler接管
*/
fun setIndicatorDrawHandler(customDrawHandler: SlidingIndicatorLayout.CustomDrawHandler?) {
indicatorLayout.customDrawHandler = customDrawHandler
}
接口為:SlidingIndicatorLayout.kt類中的 CustomDrawHandler ,提供一個(gè)draw方法,方法內(nèi)提供2個(gè)關(guān)鍵參數(shù),第一個(gè)是 SlidingIndicatorLayout 對(duì)象,第二個(gè)是,畫布canvas對(duì)象, 前者可以讓我們拿到任何想要拿的參數(shù),后者,讓我們可以動(dòng)用想象力,把想象的特效,繪制在畫布上。
interface CustomDrawHandler {
fun draw(indicatorLayout: SlidingIndicatorLayout, canvas: Canvas?)
}
var customDrawHandler: CustomDrawHandler? = null
使用方法:
class MainActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
val adapter = MyPagerAdapter(supportFragmentManager)
hankViewpager.adapter = adapter
hankViewpager.offscreenPageLimit = 3
hankViewpager.setPageTransformer(true, MyPageTransformer(this, adapter.count))
hankTabLayout.setupWithViewPager(hankViewpager, GradientTextView(this))
hankTabLayout.setIndicatorDrawHandler(CustomDrawHandlerImpl(this))
hankTabLayout2.setupWithViewPager(hankViewpager)
}
class CustomDrawHandlerImpl : SlidingIndicatorLayout.CustomDrawHandler {
val context: Context
constructor(context_: Context) {
context = context_
}
override fun draw(indicatorLayout: SlidingIndicatorLayout, canvas: Canvas?) {
val paint = Paint()
paint.color = context.resources.getColor(R.color.c1)
val fraction =
(indicatorLayout.parent.mCurrentPosition.toFloat() + 1) / indicatorLayout.childCount.toFloat()// 分?jǐn)?shù)
val left = indicatorLayout.parent.scrollX
val right =
(indicatorLayout.parent.scrollX + indicatorLayout.parent.measuredWidth * fraction).toInt()
val rect = Rect(left, 0, right, dpToPx(context, 10f))
canvas?.drawRect(rect, paint)
}
}
...
}
運(yùn)行效果請(qǐng)看下圖上半部分(下面一半仍然是用來(lái)對(duì)比),我實(shí)現(xiàn)了一個(gè)用indicator來(lái)記錄當(dāng)前滑動(dòng)的進(jìn)度的特效,只作為簡(jiǎn)單效果的展示,表示它可以實(shí)現(xiàn)任何你能想到的indicator動(dòng)效,上面的代碼,我只繪制了矩形,其實(shí)還可以繪制任何其他圖形,任你想像。
indicator特效解耦.gif
結(jié)語(yǔ)
Demo的地址為:https://github.com/18598925736/StudyTabLayout/tree/hank_v1
請(qǐng)下載運(yùn)行最新版本代碼看效果。
至此,所有內(nèi)容放送完畢,全文技術(shù)從立意到實(shí)踐編碼,再到文章出爐,歷時(shí)半月,終于功成。由于只是業(yè)余時(shí)間研究所得,細(xì)節(jié)上還沒有打磨得十分圓滿。
寫出一個(gè)類似這樣的控件并不難,技術(shù)上基本沒有什么縱深,但是涉及面很廣,而且一旦開頭的思路錯(cuò)了,后續(xù)隱患無(wú)窮。我的思維是,向源碼學(xué)習(xí),將基礎(chǔ)架構(gòu)學(xué)到手,具體的實(shí)操,我們?cè)僮约喊盐铡9雀璧淖⑨岆m然有些生澀難懂,但是大體思維,只要認(rèn)真研讀源碼,總是能得到啟發(fā)的。
希望能給其他開發(fā)者帶來(lái)新的思路和借鑒。
歡迎 看到的各位大佬留言交流,批評(píng)指正。謝過(guò)!


















