手把手講解全能型GreenTabLayout研發(fā)全攻略

前言

上一篇文章講了如何 從ViewPager的源碼入手荠卷,定義自己的ViewPager滑動特效及老。ViewPager由于有自己的動畫接口接口,我們可以直接拿到當前ItemView顶岸,以及它的position位置參數(shù)盈电,因此可以做出任何我們能夠想到的特效。

但是燎斩,TabLayout虱歪,谷歌貌似就沒有那么周到的服務,像是今日頭條那樣的TabLayout 滑動時的 文字部分顏色變化栅表,還有很多其他app中出現(xiàn)的下方橫條indicator長短變化的 特效笋鄙,還有更多其他特效。如果使用谷歌原生的TabLayout是無法做到的怪瓶,這個時候就需要我們 自定義TabLayout萧落,但是,說是自定義洗贰,前提還是要參照 谷歌的TabLayout源碼找岖,然后在其基礎(chǔ)上進行再創(chuàng)作。因為哆姻,從0開始要制作一個 和谷歌原生同樣質(zhì)量的控件宣增,包括滑動流暢度和邊界控制,并且具有良好的擴展性矛缨,并沒有那么容易爹脾,如果存在改造原生TabLayout的可能性帖旨,改造的代價要小于 從0創(chuàng)造。所以灵妨,優(yōu)先 閱讀源碼解阅,探尋這種可能性,如果沒有可能性泌霍,再去從0創(chuàng)造货抄。

Demo的地址為:https://github.com/18598925736/StudyTabLayout/tree/hank_v1

正文大綱

  • 源碼分析
  • 開發(fā)思路
  • 開始搬磚
    • 一. 尊重原著
    • 二. 聯(lián)動滑動
    • 三.特效解耦

正文

源碼分析

創(chuàng)建一個androidStudio工程,然后寫一個TabLayout+ViewPager效果朱转,之后進入看TabLayout的java源碼蟹地。源碼總長度超過了3000行。但是其中大部分都是注釋以及空行藤为,無需害怕怪与。

從我們對TabLayout UI 上的感官印象,可以得知缅疟,它應該是一個橫向可滾動的 HorizontalScrollView, 它包含兩個關(guān)鍵的元素分别,一個是文字部分的 TabView,一個是 下劃線 Indicator .

image-20200320164934056-1585550575830.png

我們的動畫也是圍繞這兩個部分存淫,所以明確這次源碼分析的最終目標:

  1. TabView 在TabLayout中是如何 添加 進去的

2)Indicator在TabLayout中是如何 繪制 進去的

TabView明顯是一個以TextView為基礎(chǔ)耘斩,所以是添加到TabLayout 中;Indicator是 一個圖形桅咆,所以應該是繪制的括授。

帶上目標來探索源碼,事半功倍轧邪。

開工刽脖。先看TabView.

從注釋中得知,TabLayout其實可以主動去addTab來添加子view,那就從 addTab方法來入手忌愚。

image-20200320165636066-1585550578377.png

注釋中有這么一句。

tabLayout.addTab(tabLayout.newTab().setText("Tab 1"));

找到一個關(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)建,并且 指定了當前TabLayout為它的parent腊徙,并且createTabView(tab)然后 給他的view屬性賦值简十。

后續(xù)還有一句:setText("Tab 1") , 在創(chuàng)建出來的Tab上調(diào)用setText方法,進入看看:

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;
}

由此得知撬腾,我們在上圖中所看到的title0 文本內(nèi)容螟蝙,就是傳到了這個方法(可以debug驗證)。那么 這里的 text 屬性用在了什么位置呢民傻?內(nèi)部類TabView的updateTextAndIcon()方法

image-20200320173530548-1585550586641.png

從這段中可以得出胰默,我們的文本內(nèi)容场斑,最終傳給了 參數(shù) textView : textView.setText(text);

追蹤updateTextAndIcon()的調(diào)用位置,看看這個textView是什么牵署。

經(jīng)過4處調(diào)用位置的檢查漏隐,

image-20200320174011680-1585550589010.png

發(fā)現(xiàn)它是:

image-20200320174106769-1585550590865.png

這兩個屬性之一。TabView的兩個TextView類型的成員變量奴迅。

那么只需要關(guān)心這兩個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 這個文本被設(shè)置到 了 內(nèi)部類TabView(一個線性布局)的TextView成員中。

所以 title0 這個文本 的所在對象暇检,從小到大浩村,依次是,

原生TextView -> 內(nèi)部類TabView -> 內(nèi)部類Tab -> 原生TabLayout

基于這樣的認知占哟,要探究一下是不是存在文字被重新繪制的可能性心墅。

要想對TextView根據(jù)需求重新繪制怎燥,那么除非可以像ViewPager一樣蜜暑,把View以及當前Position反饋到最外層。Position暫且不管隐绵,先看 最終的TextView.

經(jīng)過一番搏斗,發(fā)現(xiàn)并沒有這樣的接口拙毫。缀蹄。缺前。所以沒辦法了拯刁。TabView 在TabLayout中是如何 添加 進去的 的探索結(jié)果表明垛玻,谷歌并沒有給機會讓我們 定制文本部分的內(nèi)容特效棺牧。所以颊乘,放棄吧乏悄。

然后從頭看起,如果以 Indicator在TabLayout中是如何 繪制 進去的 為準來進行探索规求。

image-20200321201914158-1585550594385.png

這里有兩個方法,configureTab 方法丛塌,只是對tab對象進行了 保存±蚜玻看addTabView方法。

image-20200321202058037-1585550596434.png

這里的tab.view 是 TabView對象鲤桥,它最終添加到了 slidingTabIndicator 中去茶凳。而 slidingTabIndicator 它則是一個 內(nèi)部類筒狠,同樣是線性布局雇庙,方向為橫向疆前,它把TabView對象添加進去之后,多個TabView就會橫向排列胸完。而底下那一個橫向的indicator,則是由 畫筆 selectedIndicatorPaint 繪制而成誓琼。根據(jù)如下:

image-20200321203148966-1585550597890.png
image-20200321203307642-1585550599417.png

得出最終結(jié)論:TabLayout的設(shè)計布局如下圖:

image-20200321204132776-1585550603705.png

最后我探索了一下齿穗,indicator 橫條跺株,谷歌是不是有提供對外接口來編輯特效。倒是 內(nèi)部類 SlidingTabIndicator 有一個ValueAnimator indicatorAnimator 在控制 橫條滑動的位置動畫袖扛,使用的是 FastOutSlowInInterpolator 插值器唇礁。但是對我們自定義特效沒啥用。

最后結(jié)論琢融,放棄治療了。在TabLayout上奋蔚,谷歌確實不給機會。


開發(fā)思路

谷歌工程師設(shè)計的控件是針對全世界的開發(fā)者和使用者馒过,肯定會考慮周全,支持很多自定義屬性没隘,細節(jié)細致入微葫录,所以代碼看上起會顯得非常復雜骇扇,難以讀懂少孝,而且這么多英文注釋,你懂的,反正我看他們的注釋一邊看一邊猜盖淡。

然而我們的UI姐姐有自己的要求,所以如果我們可以做自己的UI控件味赃,就可以擺脫谷歌源碼的控制,隨心所欲地控制TabLayout的視覺效果。

今天本文的最終目的:

是開發(fā)一個 綠色版的 GreenTabLayout狠持,去掉谷歌原本一些繁雜的設(shè)定,增添開發(fā)常用的自定義屬性,并且開放 自定義效果的接口昭齐,讓其他開發(fā)者可以在不改動我原本代碼的前提下,編輯自己的動畫特效里覆。

上面TabLayout UI層級圖,展示了谷歌工程師的設(shè)計思路车荔,此思路沒有問題,我們可以參照它珠增。

但是一步達成最終效果不太可能,我們分階段來達成效果:

  • 尊重原著

    GreenTabLayout 必須與原TabLayout相差不大,要有文字title標題,要有indicator橫條

  • 聯(lián)動滑動

    自定義TabLayout必須能夠和ViewPager一樣苔严,產(chǎn)生同樣的聯(lián)動滑動效果,包括橫條的滑動和 標題部分的滑動

  • 特效解耦

    自定義TabLayout 把 標題欄的View覆旭,indicator橫條View寂祥,對外提供方便的動畫特效定制接口,符合開閉法則.


開始搬磚

確定了基本思路惜犀,接下來就要腳踏實地了。在Kotlin語言如此之香的潮流下撇吞,我也追求一波時尚,開發(fā)將采用Kotlin編碼颂砸,最大程度節(jié)省代碼量勤篮,使用kotlin"域"的概念隔離程序邏輯,盡可能使源碼可讀性提高瀑焦。

一. 尊重原著

要實現(xiàn)與原生TabLayout一樣的效果,可以抄谷歌的作業(yè), 原本的UI層級,照搬即可。

下載源碼之后凫乖,git checkout 4ed2 運行看效果

從外到內(nèi)有三層:

最外層

它的最外層是一個橫向可滾動的 HorizontalScrollView 的子類,同時它提供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)
    }

}       

中間層

中間層橘蜜,是一個橫向線性布局跌捆,寬度自適應姆钉,根據(jù)內(nèi)容而定,提供addTabView方法,用來添加 TabView到自身思恐,同時 繪制 indicator橫條, 橫條與當前選中的tabView等長并處于最下方.

繪制橫條可能有多種方式婚温,這里借鑒了谷歌的思路栈顷,使用Drawable.draw(canvas) ,好處就是,可以指定drawable圖片惑朦,使用圖片內(nèi)容繪制在canvas上胃珍。后續(xù)會有體現(xiàn)吩蔑。

/**
 * 中間層 可滾動的
 */
class IndicatorLayout : LinearLayout {
    constructor(ctx: Context) : super(ctx) {
        init()
    }

    private fun init() {
        setWillNotDraw(false) // 如果不這么做,它自身的draw方法就不會調(diào)用
    }

    var indicatorLeft = 0
    var indicatorRight = 0

    /**
     * 作為一個viewGroup仆潮,有可能它不會執(zhí)行自身的draw方法暑诸,這里有一個值去控制,好像是 setWillNotDraw
     */
    override fun draw(canvas: Canvas?) {
        val indicatorHeight = dpToPx(context, 4f)// 指示器高度
        // 現(xiàn)在貌似應該去畫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()) {// 如果是當前被點擊的這個冶忱,那么就不需要管
                current.setSelectedStatus(true) // 選中狀態(tài)
            } else {// 如果不是
                current.setSelectedStatus(false)// 非選中狀態(tài)
            }
        }
    }

    /**
     * 但是onDraw一定會執(zhí)行
     */
    override fun onDraw(canvas: Canvas?) {
        super.onDraw(canvas)
    }

    // 對外提供方法尾菇,添加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()
        }
    }
}

最里層

說是最里層,其實這里分為兩小層囚枪,一個是TabView(繼承線性布局)派诬,一個是TextView(用來展示 title)链沼,

提供點擊事件默赂,和狀態(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括勺,用來對比缆八,下一半是剛剛完成的效果曲掰。但是和上面的原生TabLayout比起來. 第一步完成。從開始寫代碼耀里,到完成這個效果蜈缤,一直參考的 谷歌的代碼。

二. 聯(lián)動滑動

下載源碼之后冯挎,git checkout a132 運行看效果

布局層級已經(jīng)完成底哥,現(xiàn)在需要聯(lián)動Viewpager的滑動參數(shù),讓GreenTabLayout 跟隨ViewPager一起滑動房官。

注冊監(jiān)聽

要實現(xiàn)聯(lián)動趾徽,首先要知道,谷歌源碼中翰守,TabLayout是如何與ViewPager發(fā)生聯(lián)動的孵奶,它們的聯(lián)結(jié)點在哪里,請看代碼:

tabLayout.setupWithViewPager(viewpager)

平時我們用 原生TabLayout蜡峰,兩者唯一發(fā)生交集的地方就是這里了袁,進入看源碼:

image-20200330142618611.png

顯然他們的交集可能是某個回調(diào)監(jiān)聽,順著這個線索湿颅,最終確定载绿,上面的 pageChangeListener就是 聯(lián)動滑動的交界點,這里把監(jiān)聽器傳給ViewPager油航,ViewPager則可以把自己的滑動參數(shù)傳遞給TabLayout崭庸,TabLayout則做出相應的行為。

監(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 直接加上 這個接口實現(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) {
    ...
    }
}

然后提供一個 相同的 setupWithViewPager(viewpager) 方法, 在內(nèi)部怕享,給ViewPager綁定監(jiān)聽,同時根據(jù) viewPager的adapter內(nèi)的 page數(shù)目镰踏,決定TabView的數(shù)目和每一個的標題函筋。

fun setupWithViewPager(viewPager: ViewPager) {
    this.mViewPager = viewPager
    viewPager.addOnPageChangeListener(this)// 注冊監(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ù)目和每一個標題
    }
}

參數(shù)分析

注冊監(jiān)聽之后余境,Viewpager可以把自己的滑動參數(shù)的變化告知TabLayout驻呐,但是TabLayout如何去處理這個參數(shù)變化,還需要從參數(shù)的規(guī)律上去著手芳来。重點分析 監(jiān)聽的 onPageScrolled 方法, 重點中的重點含末,則是前兩個參數(shù):position(當前page的index) 和 positionOffset(當前page的偏移百分比,小數(shù)表示的)

為了研究規(guī)律即舌,我們用上面剛剛完成的代碼把GreenTabLayout和ViewPager連結(jié)上佣盒,然后打印日志onPageScrolled

image-20200330145008704.png

基本得出一個結(jié)論:

position為0的,為當前選中的這個page顽聂,當慢慢從當前page劃走時肥惭,它的positionOffset會從0慢慢變成1

并且盯仪,如果手指分方向滑動試驗,可知:

當手指向左蜜葱,positionOffset會遞增全景,從0到極限值1,到達極限之后歸0牵囤,同時 position遞加1

反之爸黄,手指向右,positionOffset會遞減揭鳞,從1 遞減到0炕贵,從遞減的那一刻開始,position遞減1

基于上面的規(guī)律野崇,我們可以調(diào)試出 indicator橫條動畫的代碼:

...
override fun onPageScrolled(position: Int, positionOffset: Float, positionOffsetPixels: Int) {
        scrollTabLayout(position, positionOffset)
}

private fun scrollTabLayout(position: Int, positionOffset: Float) {
    // 如果手指向左劃称开,indicator橫條應該從當前位置,滑動到 下一個子view的位置上去乓梨,position應該+1
    // 如果手指向右滑動鳖轰,position立即減1,indicator橫條應該從當前位置向左滑動
        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","當前index:${position}  left:${currentLeft} right:${currentRight}  " +" 目標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ū)分滑動的方向扶镀?把日志打印出來一看就明白:

這是手指向左劃一格

image-20200330151551105.png
  • 觀察positionOffset的變化脆霎,從0 變?yōu)?,然后歸零狈惫。

  • 而看橫條的當前 left = 26,right=170鹦马, 以及 目標left=222胧谈,right=380 ,隨著positionOffset的遞增荸频,橫條會慢慢向右菱肖。

  • 而到達最后,positionOffset歸零了旭从,當前l(fā)eft 也變成了 目標的left = 222稳强,right=380.

橫條向右平移完成。

手指向右劃一格和悦,日志如下:

image-20200330152206881.png
  • position先直接減1退疫,positionOffset則從1慢慢變成0.

  • 橫條從 left=26 right=170 的起始位置,向 目標 left=222鸽素,righ=380 移動褒繁,但是由于positionOffset是遞減的,所以馍忽,橫條的移動方向反而是 向左棒坏。一直到positionOffset為0燕差,到達 left=26 right=170.

橫條向左平移也完成。

整體平移

橫條雖然可以跟著viewPager的滑動而滑動坝冕,但是如果TabView已經(jīng)排滿了當前屏幕徒探,橫條到達了當前屏幕最右側(cè),viewPager上右側(cè)還有內(nèi)容還可以讓手指向左滑動喂窟。此時测暗,就必須滾動最外層布局,來讓TabView顯示出來谎替。

通過觀察原生TabLayout偷溺,它會盡量讓 當前選中的tabView位于 控件的橫向居中的位置。而隨著 ViewPager的當前page的變化钱贯,最外層GreenTabLayout也要發(fā)生橫向滾動挫掏。

所以我選擇在 回調(diào)函數(shù)onPageSelected中執(zhí)行滾動:

    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í)行滾動的思路為:

  • 確定 當前選中的tabView的 矩形范圍 tabView.getHitRect(tabViewBounds)
  • 確定 確定最外層GreenTbaLayout的矩形范圍 getHitRect(parentBounds)
  • 計算兩個矩形的x軸的中點,然后計算出兩個中點的差值秩命,差值就是需要滾動的距離
  • 使用屬性動畫進行平滑滾動
    /**
     * 用動畫平滑更新indicator的位置
     * @param tabView 當前這個子view
     */
    fun updateIndicatorPositionByAnimator(
        tabView: GreenTabView,
        targetLeft: Int,
        targetRight: Int) {
        ...
        // 處理最外層布局( HankTabLayout )的滑動
        parent.run {
            tabView.getHitRect(tabViewBounds) //確定 當前選中的tabView的 矩形范圍
            getHitRect(parentBounds) // 確定最外層GreenTbaLayout的矩形范圍
            val scrolledX = scrollX // 已經(jīng)滑動過的距離
            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 //  差值就是需要滾動的距離

            startScrollAnimator(this, scrolledX, scrolledX + needToScrollX)
        }
    }

    /**
     * 用動畫效果平滑滾動過去
     */
    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()
    }

二階效果

完成到這里尉共,就能達成下圖中的效果:

聯(lián)動滑動.gif

上半部分為原生TabLayout效果,下把那部分為 剛剛完成的效果弃锐,幾乎沒有差別了袄友。

當然,我們這是把TabLayout本地化霹菊,完成這些剧蚣,僅僅用了kotlin 300多行代碼⌒ⅲ可見Kotlin在省代碼方面鸠按,確實是一絕,比java簡潔很多饶碘。

三.特效解耦

這一階段主要做2件事:

  • 支持開發(fā)中的常用的UI設(shè)計要求目尖,這個可以做成自定義屬性
  • 開放無耦合接口,使得開發(fā)者可以使用該接口編輯 indicator橫條 / TabView文本 的滑動特效扎运,而不用改動GreenTabLayout的內(nèi)部實現(xiàn)

第一點瑟曲,都是一些基礎(chǔ)性的改造,就不贅述了豪治,關(guān)于自定義屬性的添加和使用洞拨,都是死框架,沒什么好說的鬼吵,下面扣甲,總結(jié)一下 我所支持的所有屬性:

盤點自定義屬性

TabView標題欄部分:

屬性名 意義 取值類型
tabViewTextSize 標題字體大小 dimension|reference
tabViewTextSizeSelected 選中后的標題字體大小 dimension|reference
tabViewTextColor 標題字體顏色 color|reference
tabViewTextColorSelected 選中后的標題字體顏色 color|reference
tabViewBackgroundColor 標題區(qū)域背景色 color|reference
tabViewTextPaddingLeft 標題區(qū)內(nèi)邊距左 dimension|reference
tabViewTextPaddingRight 標題區(qū)內(nèi)邊距右 dimension|reference
tabViewTextPaddingTop 標題區(qū)內(nèi)邊距上 dimension|reference
tabViewTextPaddingBottom 標題區(qū)內(nèi)邊距下 dimension|reference
tabViewDynamicSizeWhenScrolling 是否允許滾動時的字體大小漸變 boolean

Indicator橫條部分:

屬性名 意義 取值類型
indicatorColor 橫條顏色 color|reference
indicatorLocationGravity 橫條位置 枚舉:TOP 放在頂部 / BOTTOM 放在底部
indicatorMargin 橫條間距,當indicatorLocationGravity為TOP時表示距離頂端的距離,BOTTOM時表示距離底部的距離 dimension|reference
indicatorWidthMode 橫條寬度模式 枚舉:RELATIVE_TAB_VIEW 取TabView寬度的倍數(shù) / EXACT 取精確值
indicatorWidthPercentages 橫條寬度百分比琉挖,當indicatorWidthMode 為 RELATIVE_TAB_VIEW時才會生效启泣,表示橫條寬度占TabView寬度的百分比 float(大于0)
indicatorExactWidth 橫條寬度精確值,當indicatorWidthMode 為 EXACT時才會生效示辈,表示橫條的精確寬度 dimension|reference
indicatorHeight 橫條高度 dimension|reference
indicatorAlignMode 橫條對其模式 枚舉: LEFT / CENTER / RIGHT
indicatorDrawable 橫條drawable寥茫,可以指定橫條的內(nèi)容為圖片 reference
indicatorElastic 是否開啟滾動時橫條的彈性效果 boolean
indicatorElasticBaseMultiple 當indicatorElastic開啟時生效,表示彈性倍數(shù)矾麻,數(shù)字越大纱耻,彈性越明顯 float

其中大部分屬性的處理都是基于非常基礎(chǔ)的View控件知識和簡單的數(shù)學計算险耀,只有幾點需要講解說明:

  • tabViewDynamicSizeWhenScrolling 是否允許滾動時的字體大小漸變
  • indicatorElastic 是否開啟滾動時橫條的彈性效果

這兩點弄喘,都與 viewPager滑動時的參數(shù)變化有關(guān)系,所以處理這兩個特性甩牺,需要結(jié)合參數(shù)變化規(guī)律

較復雜屬性處理

  • tabViewDynamicSizeWhenScrolling viewPager滾動時蘑志,標題的字體大小會發(fā)生漸變:
標題欄字體大小漸變.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) // 目標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) {
                // 在這里,讓當前字體變小贬派,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)時搞乏,positionOffset的變化沒有 dragging時那么細致
                        // 只要不處理 SETTING下的字體大小變化波桩,也可以達成效果
                        if (!settingFlag)
                           indicatorLayout.resetTabViewsStatueByAnimator(indicatorLayout[mCurrentPosition] as GreenTabView)
                        settingFlag = true
                    }
                }
            }
        }
    }
}

處理思路依舊是圍繞 onPageScrolled 的參數(shù)變化,核心方法為:dealAttrTabViewDynamicSizeWhenScrolling(..), 讓當前tabView的文本漸漸變小请敦,而nextTabView的文本逐漸變大镐躲。這里如果有疑問可以參照上文的 參數(shù)分析小章節(jié)。

但是侍筛,有一個坑匀油,就是當拖拽停止的時候,viewpager會有一個自動的回彈動作勾笆,如果這里沒處理好,就會出現(xiàn)桥滨,字體大小突變的情況窝爪,和我要的平滑動畫過渡不相符,所以齐媒,這里我做了一個特殊處理蒲每,當拖拽停止,也就是手指松開的時候喻括,抓準 ViewPager的 SCROLL_STATE_SETTLING 狀態(tài)剛剛進入的時機邀杏,使用屬性動畫平滑改變字體,核心代碼就是上文代碼塊中的:indicatorLayout.resetTabViewsStatueByAnimator(indicatorLayout[mCurrentPosition] as GreenTabView) 這句話可以讓 tabView的文本字體平滑地從 當前值(不確定,因為dragging狀態(tài)是用戶人為控制)望蜡,變?yōu)?目標值(這是確定值唤崭,要么是 正常狀態(tài)下的字體大小,要么是選中狀態(tài)下的字體大胁甭伞)谢肾,由此完美解決字體平滑變化的問題。

  • indicatorElastic 滾動時小泉,橫條會拉伸和回縮芦疏,也是跟隨 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 // 拉伸長度
                        }
                        else -> {// 如果到了下半段,當offset越過中值之后ratio的值
                            indicatorCriticalValue - positionOffset * baseMultiple
                        }
                    }
                } else 1f
            // 可以開始繪制
            selectedIndicator.run {
                setBounds(
                    ((centerX - indicatorWidth * ratio / 2).toInt()),
                    top,
                    ((centerX + indicatorWidth * ratio / 2).toInt()),
                    bottom
                )// 規(guī)定它的邊界
                draw(canvas!!)// 然后繪制到畫布上
            }
    ...
    
    }
    

    這一段提出來特別說明微姊,因為它代表了一種解題思路酸茴,我需要的效果是:

    viewPager滾動1格,我需要它在滾動一半的時候兢交,橫條拉伸到最長薪捍,從一半滾完的時候,橫條回縮到應該的寬度

    但是魁淳,viewPager滾1格飘诗,positionOffset的變化是從0 到1(手指向右),或者是從1到0(手指向左)界逛,我需要把positionOffset在到達0.5的時候當作一個臨界時間點昆稿,計算出 這個臨界時間點上,indicator橫條應該的長度息拜。

    關(guān)鍵在于:在臨界點0.5上溉潭,前半段的0->0.5的最終值,必須等于 后半段 0.5->1 的 開始值少欺,

    由于我是按照倍數(shù)來拉伸喳瓣,所以,原始倍率是1赞别。我還想用參數(shù)控制拉伸的程度畏陕,所以設(shè)計一個變量 baseMultiple(拉伸倍數(shù),數(shù)值越大仿滔,拉伸越明顯)

    列出公式

    • 前半段的ratio最終值 = 1(原始倍率)+ 0.5 * baseMultiple

    • 后半段的ratio值 = indicatorCriticalValue臨界值) - 0.5 * baseMultiple

    • 前半段的ratio最終值 = 后半段的ratio值

    計算得出惠毁,indicatorCriticalValue(臨界值) = 1 (原始倍率)+ baseMultiple

    于是就寫出了上面的代碼。

三階效果

說了這么多崎页,不如親眼看一眼效果更佳實在鞠绰,以上各項屬性,下面的動態(tài)圖基本都有體現(xiàn), 具體效果可以按需定制,基本可以滿足UI姐姐的各種騷操作要求,如果還不行飒焦,可以拿我的代碼自行修改,我的代碼注釋應該比谷歌大佬要親民很多蜈膨。,歡迎fork,star...

自定義屬性效果.gif

開放無耦合特效接口

為什么生出這種想法翁巍?這個是源自:ViewPager的無耦合動畫接口驴一。

Viewpager.setPageTransformer(true, MyPageTransformer(this, adapter.count))

viewPager的setPageTransformer,可以傳入一個 PageTransformer(接口)的實現(xiàn)類曙咽,從而控制ViewPager滑動時的動畫蛔趴,開發(fā)者可以自由定制效果,而不用關(guān)心ViewPager的內(nèi)部實現(xiàn)例朱。符合程序設(shè)計的開閉法則孝情,讓控件開發(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)

    /**
     * 可重寫,接收來自viewpager的position參數(shù)渔隶,做出隨心所欲的textView特效
     *
     * @param isSelected 是不是當前選中的TabView
     * @param positionOffset 偏移值   0<= positionOffset <=1
     */
    open fun handlerPositionOffset(positionOffset: Float, isSelected: Boolean) {}

    /**
     * 如果發(fā)生了滑動過程中特效殘留的情況羔挡,可以重寫此方法用來清除特效
     */
    open fun removeShader(oldPosition: Int, newOldPosition: Int) {}

    /**
     *  添加特效
     */
    open fun addShader(oldPosition: Int, newOldPosition: Int) {}

    /**
     * 通知,viewPager 即將進入setting狀態(tài)
     * @param positionOffset 當前offset
     * @param isSelected 是否是被選擇的TabView
     * @param direction 滑動方向间唉,大于0 表示向右回彈绞灼,小于0 表示向左回彈
     */
    open fun onSetting(positionOffset: Float, isSelected: Boolean, direction: Int) {}
}
class GreenTabLayout : HorizontalScrollView, ViewPager.OnPageChangeListener{
    ...
    fun <T : GreenTextView> setupWithViewPager(viewPager: ViewPager, t: T?) {
        ...
    }
}

你可以按照下面的模板使用這個接口:

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的一個子類,它的源碼是:

/**
 * 提供顏色漸變的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軸負向低矮,一個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) {
        // 如果不是初始值,那說明已經(jīng)賦值過被冒,那么用 參數(shù)positionOffset 和 它對比军掂,來得出滑動的方向
        Log.d(
            "setMatrixTranslate",
            " positionOffset:$positionOffset  isSelected:$isSelected   "
        )
        // 來,先判定滑動的方向昨悼,因為方向會決定從哪個角度
        mTranslate = if (mPositionOffset < positionOffset) {// 手指向左
            if (isSelected) {// 如果當前是選中狀態(tài)蝗锥,那么 offset會從0到1 會如何變化?
                mViewWidth * positionOffset // OK率触,沒問題终议。
            } else {
                -mViewWidth * (1 - positionOffset)
            }
        } else {// 手指向右
            if (isSelected) {// 如果當前是選中狀態(tài),那么 offset會從0到1 會如何變化葱蝗?
                -mViewWidth * (1 - positionOffset) // OK痊剖,沒問題。
            } else {
                mViewWidth * positionOffset
            }
        }
        postInvalidate()
    }

    /**
     * 由外部參數(shù)控制shader的位置
     * @param positionOffset 只會從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ù)它當前的mTranslate位置決定從哪個方向消失  mTranslate:$mTranslate")
        mTranslate = mViewWidth
        postInvalidate()
    }

    override fun addShader(direction: Int) {
        // 屬性動畫實現(xiàn)shader平滑移動
        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)到達了最右邊,那就保持你這個樣子就行了, 可是你是怎么到最右邊的找颓?
                } else
                    -mViewWidth
            }

        }
        val thisTranslate = mTranslate
        startAnimator(thisTranslate, targetTranslate)
    }

    private fun startAnimator(from: Float, targetTranslate: Float) {
        if (animator != null) animator?.cancel()
        // 屬性動畫實現(xiàn)shader平滑移動

        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
}

運行效果:請注意看下圖的上面半部分合愈,下半部分只是沒有加特效的對比。理論上,利用現(xiàn)在的參數(shù)佛析,可以定制出想要的任何效果益老,下圖只是我的一些效果測試。

文字漸變最終效果.gif

注意寸莫,使用了Shader特效之后捺萌,原本的 titleTextView字體顏色可能會失效,這是由shader機制決定的膘茎,但是依然可以用shader控制字體的顏色桃纯,運行Demo,閱讀源碼披坏,很快就能得出答案态坦。

既然這是一個開放接口,那么所能達成的效果棒拂,就不僅僅是上圖中所示, 利用 handlerPositionOffset的幾個參數(shù)伞梯,發(fā)揮想象力(或者UI姐姐發(fā)揮想象力),想要做出任何你希望的效果帚屉,只是時間問題谜诫。

Indicator接口

同樣,針對Indicator橫條的繪制攻旦,你也可以完全自定義喻旷,使用自己的實現(xiàn)方式,強制接管 原代碼中的繪制邏輯敬特。

接口在 GreenTabLayout.kt 中掰邢,入口方法為:

    /**
     * 注意,使用了此方法伟阔,傳入了非空的CustomDrawHandler實現(xiàn)類對象辣之,
     * 原本indicator的所有屬性都會失效,因為indicator的繪制工作皱炉,全部由CustomDrawHandler接管
     */
    fun setIndicatorDrawHandler(customDrawHandler: SlidingIndicatorLayout.CustomDrawHandler?) {
        indicatorLayout.customDrawHandler = customDrawHandler
    }

接口為:SlidingIndicatorLayout.kt類中的 CustomDrawHandler 怀估,提供一個draw方法,方法內(nèi)提供2個關(guān)鍵參數(shù)合搅,第一個是 SlidingIndicatorLayout 對象多搀,第二個是,畫布canvas對象, 前者可以讓我們拿到任何想要拿的參數(shù)灾部,后者康铭,讓我們可以動用想象力,把想象的特效赌髓,繪制在畫布上从藤。

    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()// 分數(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)
        }
    }

    ...
}

運行效果請看下圖上半部分(下面一半仍然是用來對比)催跪,我實現(xiàn)了一個用indicator來記錄當前滑動的進度的特效,只作為簡單效果的展示夷野,表示它可以實現(xiàn)任何你能想到的indicator動效懊蒸,上面的代碼,我只繪制了矩形悯搔,其實還可以繪制任何其他圖形骑丸,任你想像。

indicator特效解耦.gif

結(jié)語

Demo的地址為:https://github.com/18598925736/StudyTabLayout/tree/hank_v1

請下載運行最新版本代碼看效果妒貌。

至此通危,所有內(nèi)容放送完畢,全文技術(shù)從立意到實踐編碼苏揣,再到文章出爐黄鳍,歷時半月,終于功成平匈。由于只是業(yè)余時間研究所得框沟,細節(jié)上還沒有打磨得十分圓滿。

寫出一個類似這樣的控件并不難增炭,技術(shù)上基本沒有什么縱深忍燥,但是涉及面很廣,而且一旦開頭的思路錯了隙姿,后續(xù)隱患無窮梅垄。我的思維是,向源碼學習输玷,將基礎(chǔ)架構(gòu)學到手队丝,具體的實操,我們再自己把握欲鹏。谷歌的注釋雖然有些生澀難懂机久,但是大體思維,只要認真研讀源碼赔嚎,總是能得到啟發(fā)的膘盖。

希望能給其他開發(fā)者帶來新的思路和借鑒。

歡迎 看到的各位大佬留言交流尤误,批評指正侠畔。謝過!

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末损晤,一起剝皮案震驚了整個濱河市软棺,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌尤勋,老刑警劉巖喘落,帶你破解...
    沈念sama閱讀 211,194評論 6 490
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件德崭,死亡現(xiàn)場離奇詭異,居然都是意外死亡揖盘,警方通過查閱死者的電腦和手機,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 90,058評論 2 385
  • 文/潘曉璐 我一進店門锌奴,熙熙樓的掌柜王于貴愁眉苦臉地迎上來抚官,“玉大人迹鹅,你說我怎么就攤上這事。” “怎么了锅睛?”我有些...
    開封第一講書人閱讀 156,780評論 0 346
  • 文/不壞的土叔 我叫張陵,是天一觀的道長巴粪。 經(jīng)常有香客問我何址,道長,這世上最難降的妖魔是什么往枣? 我笑而不...
    開封第一講書人閱讀 56,388評論 1 283
  • 正文 為了忘掉前任伐庭,我火速辦了婚禮,結(jié)果婚禮上分冈,老公的妹妹穿的比我還像新娘圾另。我一直安慰自己,他們只是感情好雕沉,可當我...
    茶點故事閱讀 65,430評論 5 384
  • 文/花漫 我一把揭開白布集乔。 她就那樣靜靜地躺著,像睡著了一般坡椒。 火紅的嫁衣襯著肌膚如雪扰路。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 49,764評論 1 290
  • 那天倔叼,我揣著相機與錄音汗唱,去河邊找鬼。 笑死缀雳,一個胖子當著我的面吹牛渡嚣,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播肥印,決...
    沈念sama閱讀 38,907評論 3 406
  • 文/蒼蘭香墨 我猛地睜開眼识椰,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了深碱?” 一聲冷哼從身側(cè)響起腹鹉,我...
    開封第一講書人閱讀 37,679評論 0 266
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎敷硅,沒想到半個月后功咒,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體愉阎,經(jīng)...
    沈念sama閱讀 44,122評論 1 303
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 36,459評論 2 325
  • 正文 我和宋清朗相戀三年力奋,在試婚紗的時候發(fā)現(xiàn)自己被綠了榜旦。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 38,605評論 1 340
  • 序言:一個原本活蹦亂跳的男人離奇死亡景殷,死狀恐怖溅呢,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情猿挚,我是刑警寧澤咐旧,帶...
    沈念sama閱讀 34,270評論 4 329
  • 正文 年R本政府宣布,位于F島的核電站绩蜻,受9級特大地震影響铣墨,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜办绝,卻給世界環(huán)境...
    茶點故事閱讀 39,867評論 3 312
  • 文/蒙蒙 一伊约、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧八秃,春花似錦碱妆、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,734評論 0 21
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至骤肛,卻和暖如春纳本,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背腋颠。 一陣腳步聲響...
    開封第一講書人閱讀 31,961評論 1 265
  • 我被黑心中介騙來泰國打工繁成, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人淑玫。 一個月前我還...
    沈念sama閱讀 46,297評論 2 360
  • 正文 我出身青樓巾腕,卻偏偏與公主長得像,于是被迫代替她去往敵國和親絮蒿。 傳聞我的和親對象是個殘疾皇子尊搬,可洞房花燭夜當晚...
    茶點故事閱讀 43,472評論 2 348

推薦閱讀更多精彩內(nèi)容