??轉(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)航欄鞠值,首先我們來看一下效果圖媚创。
??有人看到這效果,可能會嘀咕了彤恶,不就是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了:
??在長按快速滑動的時候滚朵,放大動畫亂了,其實(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一個再走~~)