iT邦幫忙

2022 iThome 鐵人賽

DAY 30
2

大綱

  • Simple Using
  • Fixed tabs
  • Scrollable tabs
  • Anatomy
  • Key properties
  • Style

Simple Using

TabLayout

基本實作上,我們會需要 TabLayout 來包裹 TabItem,就能透過 TabLayout 來對這些 Item 進行設置

<com.google.android.material.tabs.TabLayout
    android:layout_width="match_parent"
    android:layout_height="wrap_content">
    ...
</com.google.android.material.tabs.TabLayout>

TabItem

有個小地方要注意,就是 tab 不能設置 id,若想對特定 item 進行操作與設置,就只能透過 icon、text、contentDescription 或是排列順序的 index

<com.google.android.material.tabs.TabLayout
    ...>

    <com.google.android.material.tabs.TabItem
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="@string/text_label_1"
        />

    <com.google.android.material.tabs.TabItem
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="@string/text_label_2"
        />
    <com.google.android.material.tabs.TabItem
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="@string/text_label_3"
        />

</com.google.android.material.tabs.TabLayout>

Setting tabLayout Listener

透過 addOnTabSelectedListener 設置在 tabLayout 來監聽用戶點選 tabItem 的狀態,有三種狀態可以監聽,分別為 onTabSelected ( tabItem 被選取時)、onTabUnselected( tabItem 從被選取狀態到未選取 )、onTabReselected ( 已經被選取又被點選一次 )

tabLayout.addOnTabSelectedListener(object : TabLayout.OnTabSelectedListener {

    override fun onTabSelected(tab: TabLayout.Tab?) {
                // called when a tab get selected
            }
    
    override fun onTabUnselected(tab: TabLayout.Tab?) {
                // called when a tab exits the selected state
            }
    
    override fun onTabReselected(tab: TabLayout.Tab?) {
                // called when a tab that is already
            }
})

Using tabs with ViewPager

tablayout 與 Viewpager 的設置會根據

  • 頁數、標題等動態創建 TabItem
  • 將 tab 和 位置與頁面滑動同步

所以必須先在 PagerAdapter(或子類)覆寫 getPageTitle 函數才能設置 tab Text Label

class Adapter : PagerAdapter() {
    ...
    override fun getPageTitle(position: Int): CharSequence? {
        // Return tab text label for position
    }
}

設置完 adapter 後,讓 viewPager 與 tab Layout 結合達成同步

tabLayout.setupWithViewPager(viewPager)

若想動態設置或改變 tab 的樣式,要從 tabLayout 拿取 tab

val tab = tabLayout.getTabAt(index)
tab?.icon = drawable

Using tabs with ViewPager2

使用 ViewPager2 設置 TabLayout 依賴與使用與 ViewPager 相同的概念,但實現方式不同。一切都由 TabLayoutMediator 處理

in layout :

<?xml version="1.0" encoding="utf-8"?>
<androidx.appcompat.widget.LinearLayoutCompat 
    xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="vertical">

    <com.google.android.material.tabs.TabLayout
        android:id="@+id/fixedTab"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        app:tabMode="fixed">

        <com.google.android.material.tabs.TabItem
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:icon="@drawable/ic_baseline_home_24"
            android:text="Tab1" />

        <com.google.android.material.tabs.TabItem
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:icon="@drawable/ic_baseline_search_24"
            android:text="Tab2" />

        <com.google.android.material.tabs.TabItem
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:icon="@drawable/ic_baseline_dashboard_24"
            android:text="Tab3" />

    </com.google.android.material.tabs.TabLayout>

    <androidx.viewpager2.widget.ViewPager2
        android:id="@+id/tabViewPager2"
        android:layout_width="match_parent"
        android:layout_height="match_parent" />

</androidx.appcompat.widget.LinearLayoutCompat>

in code :

  • getItemCount(): Int

為了配合 Tab ,在創建 ViewPager2 Adatper 的時候,要去覆寫當中的 getItemCount 回傳 tab 的數量

  • createFragment(position: Int): Fragment

在透過 tab 切換 Fragment 的設定,要去覆寫 createFragment 方法,依照 position 生成對應的頁面

val tabSize = binding.fixedTab.tabCount
val mViewPagerAdapter = object : FragmentStateAdapter(this) {
            override fun getItemCount(): Int {
                return tabSize
            }

            override fun createFragment(position: Int): Fragment {
                return when (position) {
                    0, 3 -> FirstTabFragment()
                    1, 4 -> SecondTabFragment()
                    2 -> ThirdTabFragment()
                    else -> FirstTabFragment()
                }
            }
        }

adapter 設定後丟給 ViewPager2 ,就可以交給 TabLayoutMediator 將 ViewPager2 與 tab 結合。在 TabLayoutMediator 中覆寫 OnConfigureTab 去設定 tab 的相關屬性 ( text、icon )

// viewpager2
binding.tabViewPager2.apply {
        adapter = mViewPagerAdapter
        orientation = ViewPager2.ORIENTATION_HORIZONTAL
}

// TabLayoutMediator
TabLayoutMediator(binding.fixedTab,binding.tabViewPager2,
    object : TabLayoutMediator.TabConfigurationStrategy {
      override fun onConfigureTab(tab: TabLayout.Tab, position: Int) {
         when (position) {
            0 -> {
              tab.text = "ConfigureTab 1"
              tab.icon = resources.getDrawable(R.drawable.ic_baseline_home_24, null)
            }
            1 -> {
              tab.text = "ConfigureTab 2"
              tab.icon =resources.getDrawable(R.drawable.ic_baseline_dashboard_24, null)
            }
            2 -> {
              tab.text = "ConfigureTab 3"
              tab.icon = resources.getDrawable(R.drawable.ic_baseline_search_24, null)
            }
          }
      }
  }).attach()

Adding badges to tabs

image alt

如果想要設置 badges 來提醒用戶目前尚未處理的訊息與任務,可在剛剛 onConfigureTab 當中,去對每個 tab 設置 badge

// Get badge from tab (or create one if none exists)
val badge1 = tab.orCreateBadge
// Customize badge
badge1.number = 11
// Remove badge from tab
tab.removeBadge()

Fixed tabs

image alt

in layout

設置上,只要將 tabLayout 的屬性 app:tabMode="fixed"

<com.google.android.material.tabs.TabLayout
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    app:tabMode="fixed">

    <com.google.android.material.tabs.TabItem
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="@string/tab_1"
        android:icon="@drawable/ic_favorite_24dp"
        />

    <com.google.android.material.tabs.TabItem
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="@string/tab_2"
        android:icon="@drawable/ic_music_24dp"
        />

    <com.google.android.material.tabs.TabItem
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="@string/tab_3"
        android:icon="@drawable/ic_search_24dp"
        />

</com.google.android.material.tabs.TabLayout>

Scrollable tabs

image alt

in layout

設置上,只要將 tabLayout 的屬性 app:tabMode="scrollable"

<com.google.android.material.tabs.TabLayout
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    app:tabMode="scrollable"
    app:tabContentStart="56dp">

    <com.google.android.material.tabs.TabItem
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="@string/tab_1"
        />

    <com.google.android.material.tabs.TabItem
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="@string/tab_2"
        />

    <com.google.android.material.tabs.TabItem
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="@string/tab_3"
        />

    <com.google.android.material.tabs.TabItem
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="@string/tab_4"
        />

    <com.google.android.material.tabs.TabItem
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="@string/tab_5"
        />

</com.google.android.material.tabs.TabLayout>

Anatomy

image alt

  1. Container
  2. Active icon (optional if there’s a label)
  3. Active text label (optional if there’s an icon)
  4. Active tab indicator
  5. Inactive icon (optional if there’s a label)
  6. Inactive text label (optional if there’s an icon)
  7. Tab item

Key properties

Container attributes

Tab item icon attributes

Tab item text label attributes

更改 tabInlineLabel 為 true,可讓 tabs 的 icon 與文字 並排在同一列

Tab item container attributes

Tab indicator attributes

tabIndicatorGravity 可以改變 indicator 的位置,其中有一個特別的值 stretch,可將 indicator 填滿整個 tab,如圖下


Style

Custom Style

屬性上可以玩的很多,如背景的 Container、tab icon、tab text、tab container、tab indicator。由於是最後一篇,就花點時間幫大家玩一些屬性,並組成一個我個人喜歡的樣式,細節可看上面的屬性列表

先從簡單的開始,單純改主題色與字型,作為基底。colorPrimary 會渲染到被選取的 tabItem,colorOnSurface 則會渲染未被選取的 tabItem。色彩配置上,若只想要透過 indicator 來表現用戶選取的狀態,那建議把 colorPrimary 與 colorOnSurface 設為相同色彩

<style name="Widget.App.TabLayout" parent="Widget.MaterialComponents.TabLayout">
        <item name="materialThemeOverlay">@style/ThemeOverlay.App.TabLayout</item>
        <item name="tabTextAppearance">@style/TextAppearance.App.Button</item>
</style>

<style name="TextAppearance.App.Button" parent="TextAppearance.MaterialComponents.Button">
        <item name="fontFamily">@font/advent_pro_medium</item>
        <item name="android:fontFamily">@font/advent_pro_medium</item>
</style>

<style name="ThemeOverlay.App.TabLayout" parent="">
        <item name="colorPrimary">@android:color/holo_orange_light</item>
        <item name="colorSurface">@color/darkBlue</item>
        <item name="colorOnSurface">@color/white</item>
</style>

進階版本,來改 indicator 與動畫的一些參數,這邊就做了一個 indicator stretch 版本的,歡迎大家 demo

<style name="Widget.App.TabLayout.indicator.stretch" parent="Widget.App.TabLayout">
        <item name="tabIndicatorGravity">stretch</item>
        <item name="tabIndicatorColor">@android:color/holo_blue_dark</item>
        <item name="tabIndicatorAnimationDuration">300</item>
        <item name="tabIndicatorAnimationMode">elastic</item>
</style>

小結

實作上,只要透過 TabLayout 與 TabItem 就能實作出一個簡單的 tabs,如果是要搭配切換畫面並實現滑動的設計,就要結合 ViewPager2,若是我上述的範例不太能理解,這邊建議看官方文檔,寫得非常清楚也有親民的中翻可看。

客製化上,tab 有非常多的屬性可以去玩,上面我示範的 Custom Style 也只能算一部分,剩下的就讓大家自己去挖掘了(推坑)。但非必要建議只改顏色或字型就好,畢竟 Material Design 很多地方都幫我們包好了,若是更動到一些關鍵屬性導致整個設計配置上出現問題,也是得不償失。建議把上述屬性表先看過後,在下手會節省很多時間。

若是對實作還是有點不懂的,這邊提供我的 Github 方便大家參考

後記

不知不覺鐵人賽已經走到尾聲了,很開心參加鐵人賽並跟好想的大家共同完賽,我卻現在才想到自我介紹一下。Hi,我是 Voss,目前在好想工作室作為進駐學員,努力專研 Android 開發技巧,自學差不多快一年的時間了,之前的本業曾任職過金融與會計等商業工作

熱愛閱讀

之所以喜愛是因為,閱讀能讓我站在不同的視野看不同的事物,例如哲學有各種不同的流派,每個流派都有自己的核心思想,例如:佛洛伊德的潛意識冰山理論、榮格的集體潛意識、斯賓諾莎的自然神學、康德的二元論、叔本華的意志與表象等等,每個哲學家都依據自己得出的經驗與實踐相互理論,沒有一個是絕對的存在,可以共存也能對立,而作為旁觀者的我,能吸收他們所得出的結論與精華,再得出自己不同的看法,我熱愛這種思考的過程

每一本書與每一次的閱讀,都讓我有更多不同的觀點與想法,熱衷於這樣的感受之中,所以閱讀也成了我生活不可或缺的一個部分,就如同笛卡耳說的:「我思故我在」

閱讀種類有哲學、文學、歷史、經濟、金融與社會等等,並在 2020 年開始撰寫閱讀心得,就隨性在 ig 創辦一個粉專「書點」用來放文章,若有興趣的歡迎來看看,雖然產量不高,但保證質量絕對是沒問題的,不會為了衝數量而亂寫(文章數量還比追蹤粉絲多)

而技術文章方面,也有稍微寫一點,有空也會持續更新,在 Medium 上面連載中,內容上算是多少紀錄了我遇到的一些困難,並花時間整理了解後做成筆記,歡迎各位大神來交流提點指教~

熱愛健身

這是我個人認為最重要的事情,目前健身也快五年了,從一個 48 kg 的瘦皮猴到現在有點肉的 70-75 kg,以前單純只是為了追求改善體態與外貌,但現在我已經離不開了,我甚至已經認為是種休閒活動了,在code寫不出來,bug找不出來,身體呈現在一種心力交瘁的狀態時,動一下後,又是一尾活龍,雖然聽起來感覺是在 c 一樣

也因為健身已久,多少對人體相關的知識有了興趣,讀了一些書並嘗試寫了一點文章

若是有興趣的同好,歡迎一起交流討論~

也許大家多少會認為健身是為了追求身材外貌或是力量又或是成為專業的選手等等。但事實不是如此,健康是練出來的!! 因為在幾百年前,人類還是在草原上追逐著獵物,過著有一餐沒一餐的日子,在當時身體素質就是活下去的本錢,這樣說不是要跟大家說去狩獵打獵啥的,而是人體在那時候與現在並沒有太大的差別,所以要保持強健的身體,是需要透過不斷的運動去維持的

可能大家會想說,每天要運動好累好麻煩什麼的,但你每天還是要吃飯跟睡覺,甚至無聊就滑滑手機追劇,差別只在於身體的本能不會逼你去運動而已,但如果你想維持身體在一個跑得動走得快的狀態,健身是不能避免的,因為隨著年齡增長肌肉流失速度與神經適應會呈現雪崩式的掉落,如果不去維持會掉落的更快

尤其現代工作型態都是久坐為主,更不用說我們工程師族群為大宗,一坐就是好幾個小時起跳,一般人很難意識到自己的身體位置出現問題,例如出現骨盆前傾或後傾、駝背、烏龜頸等等,長久下來會造成肌肉酸痛甚至傷害。解決的辦法大家可能會想到人體工學椅、升降桌、筋膜放鬆或按摩來消除痠痛,這也並非錯誤,但治標不治本,就像是程式設計一樣,因為架構的過度耦合導致後期維護的困難與麻煩,倒不如從頭開始解決

所以解決這些的辦法,就是健身運動,透過運動重新喚起你的身體神經與肌肉活動,一但你知道如何活動身體,便會立刻察覺到自己身體得狀況。例如久坐狀態的人,通常會忘了臀部該如何發力,這是因為在坐的姿勢下,臀部後側的肌群被拉長,而肌肉就像橡皮筋一樣,一旦拉得太久便會失去張力或斷裂,所以感到痠痛的我們就會翹翹二郎腿,但翹二郎腿的動作會讓骨盆骨骼直接觸碰座椅,承擔身體的垂直重量,進而尾椎受傷。最佳的解法應該是透過一些啟動臀部肌群的運動,例如深蹲、硬舉、弓箭步等常見的健身動作,或是日常踩階梯的過程,試著讓臀部去發力。在運動的過程肌肉會重新收縮,回到正確的位置,就像是我們把拉長的橡皮筋放開,讓它回到原本充滿彈性的狀態,才是正確解決肌肉酸痛的方法

訓練的目地不一定是增強肌肉,更多的是讓你重新找回身體的掌控權,大家可能會想說我現在活得好好的,手腳也都活動自如,何來的問題?一開始我也這麼認為,但在我擔任自由教練的一段時間,訓練了幾個學員,幾乎沒辦法把一個生活中簡單的動作做好,那就是「踩樓梯」。雖然聽起來很荒謬,誰不會走樓梯,應該說也許你可以走上去,但不代表你有走對,就如同你可以做出一個看起來不錯用的 App,但在架構與設計上真的是好的嗎?

有些人走樓梯是用膝蓋走,就像很多人說走樓梯傷膝蓋一樣,你這樣走當然傷啊!!吃阿鈣也救不了你。多數人不知道的是臀部是人體最強壯的肌群,所以在踩上階梯的那一個核心沒有穩住,臀部也沒有發力,最後只剩膝蓋一個人孤立無援,把你沈重的身體撐上去,久而久之就以為走樓梯是很傷身的事情(就跟寫code一樣)。從這例子可以看到,在科技越來越進步的年代,有各種便利的交通工具,人體自然不需要很強壯靈活也能活下去,但應該沒有人希望有一個不靈活強健的身體吧

如果大家都知道太久沒打 code 會失去手感跟思考邏輯,那肌肉也是同理,不論健身或是寫程式,都是身體活動的一環,都需要我們去維持甚至進步

相信大家多少看過長輩們除拐杖或坐輪椅。但這現象在鄉下出身的我眼裏,是非常罕見的,因為身邊的長輩各個務農,而我外公高齡80歲還是能下田工作,上下樓梯也沒有任何問題,甚至二頭肌比我還大(感到慚愧),當然證據不只我眼見的,國外也做了很多研究與文章,而國內有何力安博士大力推廣且告知運動對人體的各種改善與效益

若是覺得文章有點嚴肅難懂,沒關係,推薦大家一個油土伯頻道,就叫做練健康,裡面內容親民易懂,而且所教導的學員幾乎都是長輩,別以為這些長輩只會甩甩手跳公園舞,隨便硬舉跟深蹲都是破百的,真的是叫你阿罵來都屌打

話題好像有點扯遠了,但我主要是希望大家能多少重視健康上面的問題,雖然可能大家覺得自己現在還年輕,但這就跟存錢一樣,健康也是可以存的,而且越早越好。不一定要去健身房,任何運動都可以,我大學時也是在學校單槓吊了一年後才去健身房

俗話說的好:有一個健康的身體,才會有一個健康的心靈,才能產出美麗的程式碼

程式之路

而之所以轉學程式也是蠻奇妙的過程,在我大三學期,當時風行 FinTech,所以學校也跟上潮流開了一些程式設計選修課程,由於我當時覺得很酷就去了。老師也有夠認真,每隔幾個禮拜都有功課,甚至還會幫我們 codeReview,現在回想真的是有夠佛心。由於當時修課的人都是商學院的,光是會計這語言就夠痛苦,更何況是八竿子打不著的程式語言,大家就幾乎寫得很痛苦甚至退選,但當時我卻覺得很有趣,把自己腦中的想法並做出實際的作品,感受上真的很美妙,也可能是在台灣裡追求分數與成績的教育制度下壓抑太久,才讓我如此印象深刻

但當時並沒有想走程式設計這塊,畢竟還是多少對金融有興趣。直到被社會催殘幾年後,在一次偶然開啟之前大學做的作品時,想起當時的感覺,就突然覺得也許當個程式設計師也很不賴吧~

就這樣我開始自行摸索,一開始把工作上 excel 的重複性工作都寫成 VBA 腳本,省下了大量工作時間就開始趁空擋偷學程式(當薪水小偷),原本想這樣學到一個程度後去找工作,但進度卻非常緩慢,也沒有人可以詢問或交流,再加上對目前的工作厭倦了

所以開始找其他學習的管道,後來從友人那聽到好想工作室的消息,詢問並考慮一番後下定決心離職專心學習。雖然好想並不像外面的那些培訓機構或資策會等等,但這就是我想要的,因為我不想再被那制式化的課程給綁住,想走出自己的路,所以好想的環境與核心概念是我夢寐以求的,甚至可以與在業界打滾已久的大神或是同好交流,這些都不是學校或培訓機構能提供的

雖然走到現在每天還是很迷惘,思考著每天學習的方向,思考著未來求職的目標,並看著越來越瘦的錢包,說不焦慮與擔心是騙人的。但冷靜一想,回頭看看一年前的自己,我很開心做了這個決定,踏出這一步。還記得當時離職前,一個資深的學姊年約三十在知道我提出離職後,對我說了:「羨慕你還能走,我是走不了了」,但我覺得她並非走不了,而是她限縮了自己的路,可能是覺得自己是商管畢業,又或是年資已久,讓我意識到在很多層面,還是被社會的價值觀與文化所框架,害怕被否定、害怕不合群、害怕做出改變等

這世界上唯一不變的,就是變化

感謝把文章看到這裡的各位,希望我這三十天的 Material Design 之旅,能讓大家對 Android 的 UI/UX 有一些初步的了解,很可惜礙於篇幅沒能把剩下的一些元件介紹完,還有動畫那些等等,剩下的就要靠大家自己去閱讀官方文檔了,也許明年的鐵人有機會的話才來補坑吧(應該吧


上一篇
Day 29 - Tabs ( Design )
系列文
從 Google Material Design Components 來了解與實作 Android 的 UI/UX 元件設計30
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言