Android TV開發(fā)筆記(一) TV導(dǎo)航菜單

??轉(zhuǎn)發(fā)請注明出處:http://www.reibang.com/p/cf818a09f756
??在正文前來個小介紹称鳞,筆者的現(xiàn)公司原是做電視實(shí)業(yè)的占键,最近公司打算進(jìn)軍智能電視操作系統(tǒng)奢人,筆者負(fù)責(zé)前端桌面開發(fā)蓬戚,所以對于一些有異于移動端的地方饺著,做一些心得亚隙,抱著互相學(xué)習(xí)的心態(tài)定踱,如果有誤或者有更好的處理方法,請留言互相交流恃鞋,如果你喜歡本文章,小禮物就別破費(fèi)了亦歉,給個喜歡恤浪,給個關(guān)注,是對我最大的支持肴楷。
??用過智能電視的都了解水由,首頁無論如何變換,都會有一個導(dǎo)航欄赛蔫,這個導(dǎo)航欄的作用不僅僅是描述當(dāng)前頁面所屬的欄目砂客,還能讓用戶有目的性的去選擇自己所需要的分欄進(jìn)行切換泥张,可以說是桌面最主要的一個控件,今天我們分析的就是導(dǎo)航欄鞠值,首先我們來看一下效果圖媚创。

導(dǎo)航欄效果圖

??有人看到這效果,可能會嘀咕了彤恶,不就是TabLayout么钞钙,這么簡單,我三句代碼就搞定了声离,你先別忙著右上角把我×了芒炼,聽我仔細(xì)分析下。
??其一术徊,TV端與移動端最大的區(qū)別就是交互本刽,在移動端的交互主要是touch事件,而TV端則是key事件赠涮,導(dǎo)航欄不止要處理自己內(nèi)部的key事件子寓,還要與其他內(nèi)容區(qū)域銜接,例如在其上的狀態(tài)欄世囊,其下的內(nèi)容區(qū)域别瞭,同時導(dǎo)航欄自身有無焦點(diǎn)時的狀態(tài)處理,如果讓導(dǎo)航欄每個分欄都單獨(dú)處理key事件株憾,這無疑會增加很多不可控蝙寨,因?yàn)榉謾诘臄?shù)目是會改變的。
??其二嗤瞎,導(dǎo)航欄的下標(biāo)是一張UI切圖墙歪,在欄目切換的時候,做縮放旋轉(zhuǎn)位移動畫贝奇,并隨著導(dǎo)航欄有無焦點(diǎn)的狀態(tài)而顯隱虹菲,TabLayout并沒有對下標(biāo)做拓展,不去重寫的情況下掉瞳,只能用一條線并且只能控制其高度無法控制寬度毕源。
??所以綜上所述,本文將介紹適用于電視的自定義導(dǎo)航欄NavigationLinearLayout陕习,主要還是闡述思路霎褐,及在開發(fā)過程所遇到的問題。

一该镣、模塊初始化

??導(dǎo)航欄與光標(biāo)看似是一整個模塊冻璃,但實(shí)際做法,文字部分是主要組件,負(fù)責(zé)排布展示分欄和改變分欄狀態(tài)省艳,光標(biāo)則是作為一個附屬組件娘纷,只根據(jù)分欄狀態(tài)做動畫。
??初始化NavigationLinearLayout的時候在xml文件會定義好必要的屬性跋炕,這個是屬于自定義view的一些基礎(chǔ)赖晶,不熟悉的可以去找下資料,這里就不多說枣购,主要有兩個屬性需要說明:

    var orderMode: String = ""http://item排列模式嬉探,"same":固定寬模式,"self":自適應(yīng)寬模式
    var itemSpace: Int = 0//"same":item寬度棉圈,"self":item距左或右寬度(實(shí)際每個item間距是兩個itemSpace值)

??同時保存了一個map集合涩堤,存儲的是每個item中點(diǎn)到父布局NavigationLinearLayout左邊的距離:

    var mToLeftMap: MutableMap<Int, Int> = HashMap()//存儲每個item中點(diǎn)到父布局左邊的距離

??數(shù)據(jù)初始化與數(shù)據(jù)改變時重新初始化的操作是一樣的,遍歷數(shù)據(jù)長度增加刪除item分瘾,item根據(jù)xml所賦值的屬性生成并動態(tài)設(shè)置selector胎围,還原所有的狀態(tài),對item進(jìn)行賦值德召,同時對每個item的繪制做監(jiān)聽白魂,得到每個item中點(diǎn)到父布局左邊的距離,最后根據(jù)默認(rèn)展示的item進(jìn)行處理:

    private fun initView() {
        if (mToLeftMap.isNotEmpty()) mToLeftMap.clear()//還原狀態(tài)
        if (mDataList.size > childCount) {
        ...
        } else if (mDataList.size < childCount) {
        ...
        }
        if (mNowPos != -1 && mNowPos < childCount) changeItemState(mNowPos, STATE_NO_SELECT)//還原狀態(tài)
        for (i in 0..(childCount - 1)) {
            ...
            child.viewTreeObserver.addOnGlobalLayoutListener(object : ViewTreeObserver.OnGlobalLayoutListener {
                override fun onGlobalLayout() {
                    ...
                    mToLeftMap[i] = child.width / 2 + child.left + this@NavigationLinearLayout.left//每個item中點(diǎn)到父布局左邊的距離
                    if (defaultPos == i) {//TODO 如果編輯導(dǎo)航后不要重置pos上岗,可根據(jù)實(shí)際修改邏輯
                        mNowPos = defaultPos//默認(rèn)要展示的pos
                        changeItemState(mNowPos, STATE_HAS_SELECT_HAS_fOCUS)//修改默認(rèn)要展示的pos的狀態(tài)
                        mToLeftMap[mNowPos]?.let { mNavigationCursorView?.fsatJumpTo(it) }//移動光標(biāo)
                        mNavigationListener?.onNavigationChange(mNowPos, KeyEvent.KEYCODE_DPAD_LEFT)//展示內(nèi)容數(shù)據(jù)福荸,僅僅展示數(shù)據(jù),寫左右都沒問題
                    }
                }
            })
        }
    }

??要說明下肴掷,根據(jù)默認(rèn)pos的完成最后初始化做了三步操作:
??(1)changeItemState()這個方法就是改變item的狀態(tài)敬锐,狀態(tài)有三種:

    const val STATE_NO_SELECT = 666//默認(rèn)狀態(tài)
    const val STATE_HAS_SELECT_NO_fOCUS = 667//選中無焦點(diǎn)
    const val STATE_HAS_SELECT_HAS_fOCUS = 668//選中有焦點(diǎn)
    private fun changeItemState(pos: Int, state: Int) {
        ...
            when (state) {
                STATE_NO_SELECT -> {
                    //if (child.scaleX != 1f) ViewCompat.animate(child).scaleX(1f).scaleY(1f).translationZ(0f).start()//TODO BUG
                    ViewCompat.animate(child).scaleX(1f).scaleY(1f).translationZ(0f).start()
                    (child as TextView).setShadowLayer(0f, 0f, 0f, fontColorLight)
                    child.isSelected = false
                }
                STATE_HAS_SELECT_NO_fOCUS -> {
                    if (child.scaleX != 1f) ViewCompat.animate(child).scaleX(1f).scaleY(1f).translationZ(0f).start()
                    if (!child.isSelected) {
                        (child as TextView).setShadowLayer(25f, 0f, 0f, fontColorLight)
                        child.isSelected = true
                    }
                }
                STATE_HAS_SELECT_HAS_fOCUS -> {
                    ViewCompat.animate(child).scaleX(enlargeRate).scaleY(enlargeRate).translationZ(0f).start()
                    if (!child.isSelected) {
                        (child as TextView).setShadowLayer(25f, 0f, 0f, fontColorLight)
                        child.isSelected = true
                    }
                }
            }
    }

??在這遇到一個問題,在代碼里面用TODO BUG標(biāo)明呆瞻,當(dāng)焦點(diǎn)在導(dǎo)航欄台夺,比如從影視切換到教育,這時候影視分欄狀態(tài)是從STATE_HAS_SELECT_HAS_fOCUS變成STATE_NO_SELECT痴脾,起初考慮到性能問題颤介,做了判斷,只有字體放大過才做還原動畫(注釋了的那句代碼)赞赖,此時出現(xiàn)bug了:

Bug演示

??在長按快速滑動的時候滚朵,放大動畫亂了,其實(shí)這個bug就是因?yàn)閕tem在STATE_HAS_SELECT_HAS_fOCUS狀態(tài)準(zhǔn)備開始做放大動畫的瞬間前域,又馬上轉(zhuǎn)變成STATE_NO_SELECT狀態(tài)始绍,此時child.scaleX是等于1f,加了判斷導(dǎo)致縮放動畫直接忽略了话侄,而放大動畫則開始執(zhí)行,就導(dǎo)致了出現(xiàn)這個bug,解決辦法就是把判斷去掉年堆,還是交給系統(tǒng)去自己處理好了吞杭,而STATE_HAS_SELECT_NO_fOCUS與STATE_HAS_SELECT_HAS_fOCUS之間的切換因?yàn)椴簧婕暗皆趯?dǎo)航欄長按,親測過是不會出現(xiàn)那種問題变丧,即使按的速度再快芽狗,在還原之前都已經(jīng)有放大了(PS:我討厭長按,明明如此完美的邏輯)痒蓬。
??(2)移動光標(biāo)童擎,這塊在下面介紹光標(biāo)的時候再說,在這里只需要知道做了這步操作攻晒。
??(3)初始化內(nèi)容數(shù)據(jù)顾复,在這寫了一個listener回調(diào)出去專門處理與外界的邏輯,pos用于設(shè)置內(nèi)容數(shù)據(jù)鲁捏,keyCode方便控制焦點(diǎn):

    interface NavigationListener {
        /**
         * @param pos     選中的序號
         * @param keyCode 點(diǎn)擊的按鍵
         */
        fun onNavigationChange(pos: Int, keyCode: Int)
    }

二芯砸、key與focus事件設(shè)置

??并不是每個分欄單獨(dú)獲取焦點(diǎn),整個導(dǎo)航欄只有父布局NavigationLinearLayout能獲取焦點(diǎn)给梅,事件全部也由父布局處理假丧。
??key事件我把他分為三類:
??(1)切換分欄刷新數(shù)據(jù):分欄內(nèi)切換左、右动羽;
??(2)會導(dǎo)致焦點(diǎn)變化:上包帚、下、左上运吓、右上及跳出導(dǎo)航欄的左右事件等等渴邦;
??(3)其他事件:menu,source等不會導(dǎo)致焦點(diǎn)有變化的事件羽德;
??所有事件如果return true了則無系統(tǒng)按鍵音几莽,需手動調(diào)用,同樣受到系統(tǒng)設(shè)置聲音大小或靜音的控制(系統(tǒng)源碼的按鍵音也同樣是調(diào)用了此方法)宅静。

    override fun onKeyDown(keyCode: Int, event: KeyEvent?): Boolean {
        if (event?.action == KeyEvent.ACTION_DOWN) {
            when (keyCode) {
                KeyEvent.KEYCODE_DPAD_LEFT -> {
                    if (mNowPos > 0) {
                        changeItemState(mNowPos, STATE_NO_SELECT)
                        changeItemState(--mNowPos, STATE_HAS_SELECT_HAS_fOCUS)
                        mToLeftMap[mNowPos]?.let { mNavigationCursorView?.jumpTo(it) }
                        mNavigationListener?.onNavigationChange(mNowPos, keyCode)
                    }//如果有跳出導(dǎo)航欄的左右事件需求可在次此處else回調(diào)出去
                    SoundUtil.playClickSound(this@NavigationLinearLayout)
                    return true//TODO 系統(tǒng)聲音會被屏蔽掉
                }
                ...
                KeyEvent.KEYCODE_DPAD_UP, KeyEvent.KEYCODE_DPAD_DOWN -> {//TODO 方向類型的事件章蚣,不想系統(tǒng)自動找焦點(diǎn),可試試return true
                    mNavigationListener?.onNavigationChange(mNowPos, keyCode)
                }
                KeyEvent.KEYCODE_MENU -> {//TODO 非方向類型事件
                    mNavigationListener?.onNavigationChange(mNowPos, keyCode)
                    return true//TODO bug
                }
            }
        }
        ...
    }

??這里又有一個小插曲姨夹,具體原因還沒搞明白纤垂,如果menu事件不返回true,即使不做任何處理磷账,第一次按完menu鍵峭沦,就會導(dǎo)致絕大部分的按鍵事件全部失效的bug,只有再次按menu或者返回逃糟,才恢復(fù)正常吼鱼,我猜是因?yàn)橄到y(tǒng)彈了一層屬于menu的view出來蓬豁,雖然看不到,但是把最上層view改變了所以導(dǎo)致這個bug菇肃,這里我直接返回ture地粪,有需要的時候在回調(diào)處理即可。
??focus事件的處理相對簡單點(diǎn)琐谤,只做了改變item狀態(tài)及控制下標(biāo)的顯隱的操作:

    override fun onFocusChanged(gainFocus: Boolean, direction: Int, previouslyFocusedRect: Rect?) {
        changeItemState(mNowPos, if (gainFocus) STATE_HAS_SELECT_HAS_fOCUS else STATE_HAS_SELECT_NO_fOCUS)
        mNavigationCursorView?.visibility = if (gainFocus) View.VISIBLE else View.INVISIBLE
        super.onFocusChanged(gainFocus, direction, previouslyFocusedRect)
    }

三蟆技、下標(biāo)的設(shè)置

??上文提過下標(biāo)NavigationCursorView作為導(dǎo)航欄的一個組件,與activity沒有任何邏輯交互斗忌,只根據(jù)導(dǎo)航欄的切換做動畫质礼,這里把下標(biāo)單獨(dú)在layout文件里面寫一個控件,是方便控制下標(biāo)的距離導(dǎo)航欄的位置织阳,甚至可以與導(dǎo)航欄垂直居中眶蕉,作為背景展示不一樣的效果,如果是不需要光標(biāo)的時候陈哑,xml里面注釋掉控件妻坝,再把關(guān)聯(lián)的那句代碼注釋,搞定惊窖,并不需要修改其他任何的地方刽宪,再者,如果有兩個地方都需要用到導(dǎo)航欄界酒,并且不一樣動畫圣拄,直接繼承重寫一下生成動畫的方法即可,拓展起來比較方便毁欣。
??NavigationCursorView里面比較簡單庇谆,只有3個方法,fsatJumpTo()方法是初始化的時候用的凭疮,jumpTo()是正常切換的時候調(diào)用饭耳,還有一個就是createAnimator()生成動畫的方法。
??每次初始化或切換時执解,會傳目標(biāo)分欄中點(diǎn)距離NavigationLinearLayout左邊的值寞肖,用于確定光標(biāo)中點(diǎn)做位移動畫的目標(biāo)位置,同時保存本地作為下次位移的初始值使用衰腌,實(shí)際上做動畫的時候新蟆,由于動畫的相對坐標(biāo)是控件的左上角(0,0)坐標(biāo),因此實(shí)際位置還需減去光標(biāo)的寬度的一半右蕊,才是設(shè)置給位移動畫的目標(biāo)位移值琼稻。

    val realLocation = location - width / 2

四、在activity的調(diào)用

??調(diào)用非常簡單饶囚,就三行代碼帕翻,調(diào)用順序已經(jīng)做了兼容處理所以怎么調(diào)都行鸠补,光標(biāo)默認(rèn)是隱藏可以在需要的時候再設(shè)置展示出來,也可以先初始化好導(dǎo)航欄熊咽,需要設(shè)置數(shù)據(jù)的時候再設(shè)置監(jiān)聽(這個是YY出來的莫鸭,一般沒這種需求吧)。

    mNavigationLinearLayout_id.mDataList = arrayListOf("我的電視", "影視", ...)
    mNavigationLinearLayout_id.mNavigationListener = mNavigationListener
    mNavigationLinearLayout_id.mNavigationCursorView = mNavigationCursorView_id

??同時還模擬了在內(nèi)容區(qū)域切換分欄横殴,分欄切換時刷新內(nèi)容區(qū)域,模擬用戶重新編輯導(dǎo)航欄數(shù)據(jù)后刷新的場景卿拴,這里的狀態(tài)欄跟內(nèi)容區(qū)域只用了一個TextView模擬衫仑,實(shí)際上是要復(fù)雜得多,這個就看各自的產(chǎn)品需求然后各自各精彩吧堕花。

五文狱、后記

??到此整個控件已經(jīng)介紹完畢,再砸一個彩蛋缘挽,如果你的需求是導(dǎo)航欄不止一屏瞄崇,需要滑動的話,臣妾做不到壕曼!這就是把整個導(dǎo)航欄當(dāng)作一個view來獲取焦點(diǎn)的弊端苏研,后期有空再研究改進(jìn),接下來要先寫桌面開發(fā)的其他模塊了腮郊,畢竟公司的開發(fā)進(jìn)度要緊摹蘑,后面還會整理然后寫一系列關(guān)于TV開發(fā)的文章。

整體效果圖演示

??最重要的當(dāng)然是效果圖及源碼啦轧飞,沒源碼說個蛋衅鹿,是吧。
??源碼截我Java和Kotlin雙版本 (還不習(xí)慣Kotln的可以看Java版源碼)
??(都看到這了过咬,客官何不star一個再走~~)

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末大渤,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子掸绞,更是在濱河造成了極大的恐慌泵三,老刑警劉巖,帶你破解...
    沈念sama閱讀 221,198評論 6 514
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件集漾,死亡現(xiàn)場離奇詭異切黔,居然都是意外死亡,警方通過查閱死者的電腦和手機(jī)具篇,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 94,334評論 3 398
  • 文/潘曉璐 我一進(jìn)店門纬霞,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人驱显,你說我怎么就攤上這事诗芜⊥ィ” “怎么了?”我有些...
    開封第一講書人閱讀 167,643評論 0 360
  • 文/不壞的土叔 我叫張陵伏恐,是天一觀的道長孩哑。 經(jīng)常有香客問我,道長翠桦,這世上最難降的妖魔是什么横蜒? 我笑而不...
    開封第一講書人閱讀 59,495評論 1 296
  • 正文 為了忘掉前任,我火速辦了婚禮销凑,結(jié)果婚禮上丛晌,老公的妹妹穿的比我還像新娘。我一直安慰自己斗幼,他們只是感情好澎蛛,可當(dāng)我...
    茶點(diǎn)故事閱讀 68,502評論 6 397
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著蜕窿,像睡著了一般谋逻。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上桐经,一...
    開封第一講書人閱讀 52,156評論 1 308
  • 那天毁兆,我揣著相機(jī)與錄音,去河邊找鬼次询。 笑死荧恍,一個胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的屯吊。 我是一名探鬼主播送巡,決...
    沈念sama閱讀 40,743評論 3 421
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼盒卸!你這毒婦竟也來了骗爆?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 39,659評論 0 276
  • 序言:老撾萬榮一對情侶失蹤蔽介,失蹤者是張志新(化名)和其女友劉穎摘投,沒想到半個月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體虹蓄,經(jīng)...
    沈念sama閱讀 46,200評論 1 319
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡犀呼,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 38,282評論 3 340
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了薇组。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片外臂。...
    茶點(diǎn)故事閱讀 40,424評論 1 352
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖律胀,靈堂內(nèi)的尸體忽然破棺而出宋光,到底是詐尸還是另有隱情貌矿,我是刑警寧澤,帶...
    沈念sama閱讀 36,107評論 5 349
  • 正文 年R本政府宣布罪佳,位于F島的核電站逛漫,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏赘艳。R本人自食惡果不足惜酌毡,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,789評論 3 333
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望蕾管。 院中可真熱鬧阔馋,春花似錦、人聲如沸娇掏。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,264評論 0 23
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽婴梧。三九已至,卻和暖如春客蹋,著一層夾襖步出監(jiān)牢的瞬間塞蹭,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 33,390評論 1 271
  • 我被黑心中介騙來泰國打工讶坯, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留番电,地道東北人。 一個月前我還...
    沈念sama閱讀 48,798評論 3 376
  • 正文 我出身青樓辆琅,卻偏偏與公主長得像漱办,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子婉烟,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 45,435評論 2 359

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