Android: Kotlin 材料设计入门
原文:Android: Kotlin 材料設計入門
作者:Joe Howard
譯者:kmyhy
更新說明:本教程由 Joe Howard 升級為 Kotlin。原教程作者是 Megha Bambra。
谷歌的材料設計使 Android app 的可視化外觀以一種令人興奮的方式亮瞎了用戶的眼睛。但稍安勿躁——什么是材料設計?
根據谷歌的描述,它“在創建漂亮直觀的體驗的同時融匯了觸感的外觀、醒目的圖形設計以及流暢的動畫”。材料設計是 Android app 的“用戶體驗哲學”。
在本教程中,你將把材料設計添加到一個叫做 Travel Wishlist 的 app 中。在這個過程中,你將學習:
- 如何實現材料主題;
- 用 RecyclerView 和 CardView 之類的 widget 構建動態視圖;
- 用 Palette API 生成顏色主題并用于文字和背景色;
- 用 Android animation API 創建精彩的交互。
本教程假定你熟悉基本的 Android 編程,包括 Kotlin、XML、Android Studio 和 Gradle。如果你是一個新手,你可以先閱讀我們的 Android 開發入門教程系列 和 Kotlin 入門。
要完成本教程,你必須使用 Android Studio 3.0 Beta 2 以上,以及 Kotlin 1.1.4-2 以上。
讓我們開始吧!
開始
下載 開始項目,打開 Android Studio。
要導入這個開始項目,首先選擇 Android Studio 歡迎界面中的 Open an existing Android Studio Project。
然后選擇已經下載的項目,再點 Open:
Travel Wishlist 是一個非常簡單的 app。用戶會看到一個網格列出了世界各地的圖片,可以通過點擊某張圖片來添加筆記,表示你想看的東西和想做的事情。
Build & run,你會看到一個只包含了基本界面的窗口:
現在,世界還是空的!你將在項目中加入一些材料組件,包括 dynamic view、color scheme 和 animations,它們會為你數據庫中的圖片增色不少!
打開 build.gradle 添加 RecyclerView、CardView、Paletter 和 Picasso 到依賴中:
dependencies {implementation fileTree(dir: 'libs', include: ['*.jar'])implementation "org.jetbrains.kotlin:kotlin-stdlib-jre7:$kotlin_version"implementation 'com.android.support:appcompat-v7:26.0.1'implementation 'com.android.support:recyclerview-v7:26.0.1'implementation 'com.android.support:cardview-v7:26.0.1'implementation 'com.android.support:palette-v7:26.0.1'implementation 'com.squareup.picasso:picasso:2.5.2'testImplementation 'junit:junit:4.12'androidTestImplementation 'com.android.support.test:runner:1.0.0'androidTestImplementation 'com.android.support.test.espresso:espresso-core:3.0.0' }這里,你直接聲明了接下來要在教程中用到的依賴。一開頭是 Google 提供的 API,但最后一個 Picasso 是由 Square 中的一群人提供的一個精彩的圖片下載和緩存庫。
聲明完依賴,就可以將材料設計導入到 app 中了!
創建主題
在繼續后面的內容之前,你需要修改主題。打開位于 res/values 目錄下的 style.xml 。默認主題是 Theme.AppCompat.Light.DarkActionBar。在 theme 標簽之內添加下列內容:
<item name="android:navigationBarColor">@color/primary_dark</item> <item name="android:displayOptions">disableHome</item>Android 自動將 colorPrimary 應用到 action bar,將 colorPrimaryDark 應用到狀態欄,將 colorAccent 應用到 UI widget 比如 text field 和 checkbox。
在上面的代碼中,你改變了 navigation bar 的顏色。對于 android:displayOptions,你設置為 disableHome 以適應屏幕布局。
Build & run,你會看到 app 變成了新的顏色主題。
這只是一個小小改變,就像在 Travel Wishlist 中的每次旅程,修改設計都是以一個簡單的步驟開始。
使用 RecyclerView 和 CardView
為了給用戶能夠看到所有他們想去的地方,你需要一個視圖。你可以用 RecyclerView 代替 ListView,它的功能要比后者強太多。Google 將 RecyclerView 稱作“一個靈活的、能夠為大數據及提供有限窗口的視圖”。在這一節,你將看到如何在數據源相同(指定用戶坐標)的情況下,將列表視圖切換到一個自定義的網格視圖。
用 XML 定義一個 RecyclerView
首先,打開 activity_main.xml,在 LinearLayout 標簽內添加:
<android.support.v7.widget.RecyclerView android:id="@+id/list"android:layout_width="match_parent"android:layout_height="match_parent"android:background="@color/light_gray"/>這里,你添加了一個 RecyclerView 到 acitivity 中,讓它占據父 view 的所有空間。
初始化 RecyclerView 并使用 LayoutManager
在編寫 Kotlin 代碼之前,需要配置 Android Studio 以便它自動插入 import 語句,節省你的時間。
找到菜單 Preferences\Editor\General\Auto Import 然后勾選 Add unambiguous imports on the fly 選項。在 MainActivity.kt 文件中,在類頭部添加:
lateinit private var staggeredLayoutManager: StaggeredGridLayoutManager簡單聲明了一個屬性用于保存 LayoutManager。
然后,在 onCreate() 方法底下添加:
staggeredLayoutManager = StaggeredGridLayoutManager(1, StaggeredGridLayoutManager.VERTICAL) list.layoutManager = staggeredLayoutManager在上面的代碼中,你將 RecyclerView 的布局管理器設置為一個 StaggeredGreidLayoutManager,這種管理器允許你創建兩種方式排列的網格。這里,你使用第一種類型,然后將 span count 設置為 1,將方向設置為 StaggeredGridLayoutMananger.VERTICAL。span count 為 1 將顯示列表而不是網格,等會你會看到。稍后,你會用兩列來設置網格的緊湊格式。
注意你使用了 Kotlin 的 Android 擴展來找到表格,因此不需要調用 findViewById()。確認下列行在你的導入語句中存在,當你輸入 list 的時候它應該是自動添加的:
import kotlinx.android.synthetic.main.activity_main.*用 CardView 創建行和單元格
CardView 為你的視圖提供了一種一致的背景,包括圓角和陰影。你將用它作為 RecyclerView 的行或單元格的布局。默認,CardView 繼承了 FrameLayout,因此它能夠包含其他子視圖。
在 res\layout 目錄中,新建一個布局文件,叫做 row_places.xml。點擊 OK 新建。
為了創建你想要的單元格布局,將整個文件的代碼提換為:
<?xml version="1.0" encoding="utf-8"?> <android.support.v7.widget.CardView xmlns:android="http://schemas.android.com/apk/res/android"xmlns:card_view="http://schemas.android.com/apk/res-auto"android:id="@+id/placeCard"android:layout_width="match_parent"android:layout_height="wrap_content"android:layout_margin="8dp"card_view:cardCornerRadius="@dimen/card_corner_radius"card_view:cardElevation="@dimen/card_elevation"><ImageView android:id="@+id/placeImage"android:layout_width="match_parent"android:layout_height="200dp"android:scaleType="centerCrop" /><!-- Used for the ripple effect on touch --><LinearLayout android:id="@+id/placeHolder"android:layout_width="match_parent"android:layout_height="match_parent"android:background="?android:selectableItemBackground"android:orientation="horizontal" /><LinearLayout android:id="@+id/placeNameHolder"android:layout_width="match_parent"android:layout_height="45dp"android:layout_gravity="bottom"android:orientation="horizontal"><TextView android:id="@+id/placeName"android:layout_width="match_parent"android:layout_height="wrap_content"android:layout_gravity="center_vertical"android:gravity="start"android:paddingStart="10dp"android:paddingEnd="10dp"android:textAppearance="?android:attr/textAppearanceLarge"android:textColor="@android:color/white" /></LinearLayout></android.support.v7.widget.CardView>通過 xmlns:card_view=”http://schemas.android.com/apk/res-auto” 這一行,你可以分配像 card_view:cardCornerRadius 和 card_view:cardElevation 這樣的屬性,它們負責給 Android App 添加符合 card 外觀的材料設計。
注意 placeHolder,你添加了一個 ?android:selectableItemBackground 作為它的背景。當用戶點擊單元格時,這會開啟一個水波紋動畫特效,就像許多 app 一樣。待會你會看到。
為 RecyclerView 實現一個 Adapter
你將通過一個 adapter 將 RecyclerView 和數據綁定。在 main/java 文件夾,右擊 com.raywenderlich.android.travelwishlist 包,選擇 New\Kotline File/Class。類名就叫做 TravelListAdapter。
在類中添加代碼,注意保留文件頭部的 package 語句:
// 1 class TravelListAdapter(private var context: Context) : RecyclerView.Adapter<TravelListAdapter.ViewHolder>() {override fun getItemCount(): Int {}override fun onCreateViewHolder(parent: ViewGroup?, viewType: Int): ViewHolder {}override fun onBindViewHolder(holder: ViewHolder?, position: Int) {}// 2inner class ViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) { } }上述代碼分為幾個步驟:
將 TravelListAdapter 中的 RecyclerView.Adapter 方法修改為:
// 1 override fun getItemCount() = PlaceData.placeList().size// 2 override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {val itemView = LayoutInflater.from(parent.context).inflate(R.layout.row_places, parent, false)return ViewHolder(itemView) }// 3 override fun onBindViewHolder(holder: ViewHolder, position: Int) {val place = PlaceData.placeList()[position]holder.itemView.placeName.text = place.namePicasso.with(context).load(place.getImageResourceId(context)).into(holder.itemView.placeImage) }代碼解釋如下:
在 MainActivity 中添加一個字段,用于保存你的 Adapter 對象:
lateinit private var adapter: TravelListAdapter然后在 onCreate() 方法配置完 LayoutManager 之后創建一個 adapter 對象,將它傳遞給 RecyclerView:
adapter = TravelListAdapter(this) list.adapter = adapterBuild & run,你會看見列表中的地點渲染出來了。
那個地方在呼喚著你了?我喜歡天藍色的水。但無論你想去哪兒,你都可以通過記錄你想去做什么來描繪你的夢想。首先,你需要讓單元格能夠響應用戶觸摸。
實現每個單元格的點擊事件
和 ListView 不同,RecyclerView 沒有 onItemClick 接口,因此你必須在 adapter 中實現一個。在 TravelListAdapter 中,增加一個屬性用于保存 OnItemClickListener 對象。在 TravelListAdapter 頂部添加代碼:
lateinit var itemClickListener: OnItemClickListener現在來實現 View.OnClickListener,以便為類內部的 ViewHolder 增加一個接口:
inner class ViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView), View.OnClickListener {然后在 ViewHolder 內部類中添加這個方法:
override fun onClick(view: View) { }在 ViewHolder 頭部添加 init 方法將二者綁定:
init {itemView.placeHolder.setOnClickListener(this) }這里,你調用了 placeholder 的 setOnClickListener 并覆蓋了 onClick 方法。
對于 RecyclerView 你必須在 onClick 方法中繼續編寫代碼。首先,為內部類 ViewHolder 定義一個接口:
interface OnItemClickListener {fun onItemClick(view: View, position: Int) }然后,為 TravelListAdapter 添加 onClickListener 的 setter 方法:
fun setOnItemClickListener(itemClickListener: OnItemClickListener) {this.itemClickListener = itemClickListener }在內部類 ViewHolder 中實現 onClick() 的邏輯:
override fun onClick(view: View) = itemClickListener.onItemClick(itemView, adapterPosition)在 MainActivity,在 onCreate() 之前創建一個 OnItemClickListener:
private val onItemClickListener = object : TravelListAdapter.OnItemClickListener {override fun onItemClick(view: View, position: Int) {Toast.makeText(this@MainActivity, "Clicked " + position, Toast.LENGTH_SHORT).show()} }最后,在 onCreate() 方法最后,就在你設置 adapter 的后面,設置 adapter 的 listener :
adapter.setOnItemClickListener(onItemClickListener)Build & run。
現在,你點擊單元格,你會看到水波紋效果,然后顯示一個 toast:
在列表和網格之間切換
StaggeredLayoutManager 為你的布局添加了靈活性。要將你現有的列表切換成更緊湊的兩列式網格,你只需要在 MainActivity 中修改 StaggeredLayoutManager 的 spanCount。
在 toggle() 方法中,在 showGridView() 之前添加:
staggeredLayoutManager.spanCount = 2然后在 showListView 頂部添加:
staggeredLayoutManager.spanCount = 1你只是簡單地將 spanCount 在 1 和 2 之間切換,它就會顯示成單列或者雙列了。
Build & run,用 action bar 按鈕在列表和網格之間切換。
在列表中使用 Palette API
現在你可以添加一些有趣的材料設計特性了,首先從 Palette API 開始。回到 TravelListAdapter,你將在那里定義 placeNameHolder 的背景色,它將動態地使用圖片中的顏色。
在 onBindViewHolder(…) 方法底部添加:
val photo = BitmapFactory.decodeResource(context.resources, place.getImageResourceId(context)) Palette.from(photo).generate { palette ->val bgColor = palette.getMutedColor(ContextCompat.getColor(context, android.R.color.black))holder.itemView.placeNameHolder.setBackgroundColor(bgColor) }generate(…) 方法創建一個用于背景的 palette,同時要傳入一個 lambda 表達式用于當 palette 成功創建后調用。在這里,你可以拿到生成的 palette 并設置 holder.itemView.placeNameHolder 的背景色。如果顏色不存在,這個方法會使用一個替代色——這里也就是 android.R.color.black。
Build & run,看看實際的效果!
注意:Palette API 會從一張圖片中獲取如下顏色配置:
- Vibrant
- Dark Vibrant
- Light Vibrant
- Muted
- Dark Muted
- Light Muted
為建議你實際體驗一下這些值。除了 palette.getMutedColor(…),你可以替換成 palette.getVibrantColor(…) 和 palette.getDarkVibrantColor(…)等。
使用新的材料 API
在這一節,你將用到 DetailActivity 和它對應的 activity_detail.xml,通過加入一些新的 材料設計 API 使它們變得更酷。
首先,你可能需要看一下開始項目中 detail view 當前的樣子。要看到這個,首先需要在 DetailActivity 的 companion 對象中添加下列代碼:
fun newIntent(context: Context, position: Int): Intent {val intent = Intent(context, DetailActivity::class.java)intent.putExtra(EXTRA_PARAM_ID, position)return intent }然后,打開 MainActivity,將位于 onItemClickListener 的 onItemClick(…) 方法中的 Toast 替換成:
startActivity(DetailActivity.newIntent(this@MainActivity, position))你可以將 place 對象的位置通過 intent 進行傳遞,這樣 DetailActivity 就能獲得信息用于布局 UI。也就是你目前所做的。
Build & run。
這里沒有任何讓人驚喜的東西,但是一個很好的基礎,在此基礎上開始添加你極度期望的材料設計 API。你還會看到一個酷酷的 FloatingActionButton,這是材料設計中包含的一個 widget。
添加呈現動畫
現在,你想讓用戶能夠添加一些文字記錄他們想去這些景色優美的地方做什么。因此,在 activity_detail.xml 中有一個隱藏的 edittext。當用戶點擊 FAB 按鈕,它才會以一個漂亮的動畫顯示出來:
打開 DetailActivity。你需要實現兩個方法:
- revealEditText()
- hideEditText()
首先,在 revealEditText() 方法中添加:
val cx = view.right - 30 val cy = view.bottom - 60 val finalRadius = Math.max(view.width, view.height) val anim = ViewAnimationUtils.createCircularReveal(view, cx, cy, 0f, finalRadius.toFloat()) view.visibility = View.VISIBLE isEditTextVisible = true anim.start()用兩個 int 值在 view 的 x,y 坐標上加一些偏移。這會產生一種動畫是從 FAB 方向開始的效果。
然后,用一個半徑使動畫呈現出環形效果,如上面的 gif 圖中所示。所有這些值——x,y 和半徑——都被傳遞給動畫對象。動畫對象用 ViewAnimationUtils 創建,它允許你創建一種環形呈現動畫。
因為 EditText 一開始是隱藏的,你將 view 的 visibility 設置為 VISIBLE,將 isEditTextVisible 設置為 true。最后,你調用了動畫對象的 start() 方法。
要解散視圖,在 hideEditText() 中添加:
val cx = view.right - 30 val cy = view.bottom - 60 val initialRadius = view.width val anim = ViewAnimationUtils.createCircularReveal(view, cx, cy, initialRadius.toFloat(), 0f) anim.addListener(object : AnimatorListenerAdapter() {override fun onAnimationEnd(animation: Animator) {super.onAnimationEnd(animation)view.visibility = View.INVISIBLE} }) isEditTextVisible = false anim.start()你的目的是隱藏view 并從反方向展現環形動畫。因此,你將初始半徑設置為 view 的寬度,終止半徑為 0,就會使圓變小了。
你先顯示動畫然后隱藏視圖。為此,你實現了一個動畫監聽器,當動畫結束就隱藏視圖。
現在 build & run,看看動畫效果!
注意:如果鍵盤彈出,你必須隱藏它才能看到完整的效果。將 DetailActivity 中的 inputManager.showSoftInput(…) 注釋掉,待會別忘了將注釋取消。呃,你的按鈕沒有顯示 + 號圖標,不用擔心,待會你會解決這個問題。
在 FAB 按鈕上進行貝塞爾變形
現在你已經完成了 edit text field 的顯示動畫和隱藏動畫,你可以將 FAB 上的圖標進行調整,使它看起來如下所示:
開始項目已經包含了 + 號和 √ 號的矢量路徑。你將看到如何將 + 號動畫或變形成 √ 號,以及相反動作。
在 res/drawables 目錄,用 New\Drawable resource file 新建一個資源文件,命名為 icn_morph,然后將根元素定義為 animated-vector:
<?xml version="1.0" encoding="utf-8"?> <animated-vector xmlns:android="http://schemas.android.com/apk/res/android"android:drawable="@drawable/icn_add"> </animated-vector>animated-vector 需要有一個 android:drawable。這個 animated vector 將從 + 號開始變形成 √ 號,因此你需要將 drawable 設置為 icn_add。
現在來進行真正的變形,在 animated-vector 標簽中添加:
<target android:animation="@anim/path_morph"android:name="sm_vertical_line" /> <target android:animation="@anim/path_morph_lg"android:name="lg_vertical_line" /> <target android:animation="@anim/fade_out"android:name="horizontal_line" />這里,你實際上將 + 號的一豎變化成 √ 號,同時消除掉一橫,如下圖所示:
然后,將一豎分成兩個路徑,一段短一段長:
從上圖中你會看到,你可以將前兩個 target:sm_vertical_line 和 lg_vertical_line 的路徑通過修改角度的方式轉換成一個 √,也就是通過上述代碼來進行的,同時隱藏 horizontal_line。
接下來,你還要逆轉動畫,將 √ 變回 +。新建一個 drawable resource 文件,叫做 icn_morph_reverse,編輯其內容為:
<?xml version="1.0" encoding="utf-8"?> <animated-vector xmlns:android="http://schemas.android.com/apk/res/android"android:drawable="@drawable/icn_add"><target android:animation="@anim/path_morph_reverse"android:name="sm_vertical_line"/><target android:animation="@anim/path_morph_lg_reverse"android:name="lg_vertical_line" /><target android:animation="@anim/fade_in"android:name="horizontal_line" /> </animated-vector>構成 + 號一豎的兩段線段又變回了原來的狀態,而一橫重新顯示,產生一個平滑效果。
現在,來完成這個動畫。打開 DetailActivity.kt,在 onClick() 中 if 分支最后及 else 分支之前添加:
addButton.setImageResource(R.drawable.icn_morph) val animatable = addButton.drawable as Animatable animatable.start()這里,你將按鈕的 image resource 設置成前面新建的 icn_morph,從 drawable 中抽取 animatable,然后播放動畫。
然后,在 else 分支的底部添加:
addButton.setImageResource(R.drawable.icn_morph_reverse) val animatable = addButton.drawable as Animatable animatable.start()基本上和前面一樣,只不過 image resource 變成了 icn_morph_reverse 而已,這樣將播放反序動畫。
除了圖標的變化,當用戶點擊時也會將文本從 todoText 添加到 toDoAdapter,同時刷新地點活動列表。因為文字是白色的,所以你看不見,但你將在下一節為視圖添加一個鮮艷的顏色讓文字變得顯眼。
Buid & run,觀察眼前的變化。FAB 圖標被點擊時會在 + 和 √ 之間變化。
用 Palette API 添加動態顏色
是時候用 Palette API 給視圖添加一些顏色了。不僅僅是和之前一樣的顏色,而且會是動態的顏色!
在 DetailActivity 中,在 colorize() 添加:
val palette = Palette.from(photo).generate() applyPalette(palette)就像你前面所做的,從一張照片中生成一個 palette——盡管這次是以同步方式——然后將 palette 傳給 applyPalette。將 applyPalette() 中的代碼替換為:
private fun applyPalette(palette: Palette) {window.setBackgroundDrawable(ColorDrawable(palette.getDarkMutedColor(defaultColor)))placeNameHolder.setBackgroundColor(palette.getMutedColor(defaultColor))revealView.setBackgroundColor(palette.getLightVibrantColor(defaultColor))}這里你使用了柔和的暗色調、柔和色調以及明亮的鮮艷色調分別作為 window、placeNameHolder 和 revealView 的背景色。
最后,閉合這個事件鏈,在 getPhoto() 方法最后添加:
colorize(photo)再次 build & run!看看 detail activity 使用對應圖片中的 palette 系統的配色方式。
用共享元素進行 Acitivity 動畫轉換
我們在 Google 的 app 中看到并贊嘆不已的圖片和文字動畫都已經在材料設計中得到了改進。立馬你就會學習這些平滑動畫的細節。
注意:包含共享元素的 acitivity 動畫,允許你的 app 在兩個共享視圖的 activity 之間進行動畫。例如,你可以在一個 detail acitivity 上將列表上的縮略圖轉換到大圖,同時保持內容的連續性。
在地名列表、MainActivity 和地名詳情視圖、DetailActivity 之間,你將對下列元素進行動畫轉換:
- 地點的圖片
- 地點的名字
- 名字后面的背景區域
打開 row_paces.xml ,在 id 為 placeImage 的 ImageView 標簽中添加聲明:
android:transitionName="tImage"然后在 id 為 placeNameHolder 的 LinearLayout 標簽中添加:
android:transitionName="tNameHolder"注意 placeName 沒有 transitionName。因為它是 placeNameHolder 的子元素,placeNameHolder 會對所有子視圖進行動畫轉換。
在 activity_detail.xml 中,在 id 為 placeImage 的 ImageView 中添加一個 transitionName:
android:transitionName="tImage"同樣,在 placeNameHolder 的 LinearLayout 標簽中添加一個 transitionName:
android:transitionName="tNameHolder"你想轉換的 activity 之間的共享元素應當用同樣的 android:transitionName,也就是你在這里所設置的。另外,請注意這張圖片的大小,以及 placeNameHolder 的高度比 activity 要大。你將在 activity 轉換過程中以動畫方式修改布局以提供一種看起來比較漂亮的連續效果。
將 MainActivity 中的 onItemClickListener() 方法修改為:
override fun onItemClick(view: View, position: Int) {val intent = DetailActivity.newIntent(this@MainActivity, position)// 1val placeImage = view.findViewById<ImageView>(R.id.placeImage)val placeNameHolder = view.findViewById<LinearLayout>(R.id.placeNameHolder)// 2val imagePair = Pair.create(placeImage as View, "tImage")val holderPair = Pair.create(placeNameHolder as View, "tNameHolder")// 3val options = ActivityOptionsCompat.makeSceneTransitionAnimation(this@MainActivity,imagePair, holderPair)ActivityCompat.startActivity(this@MainActivity, intent, options.toBundle()) }添加這段代碼之后,你需要手動添加 import 語句,因為 Android Studio 無法自動導入這個包:
import android.support.v4.util.Pair有幾個地方需要注意:
Build & run,看看從 MainActivity 切換到 DetailActivity 時的圖片動畫:
但是,有兩個地方的動畫有點跳躍:
- FAB 按鈕會楚然出現在 DetailActivity 上。
- 如果你點擊 action 或者 navigation bar 下面的行,這行會在動畫之前跳一下。
首先解決 FAB 問題。打開 DetailActivity.kt,在 windowTransition() 方法中添加:
window.enterTransition.addListener(object : Transition.TransitionListener {override fun onTransitionEnd(transition: Transition) {addButton.animate().alpha(1.0f)window.enterTransition.removeListener(this)}override fun onTransitionResume(transition: Transition) { }override fun onTransitionPause(transition: Transition) { }override fun onTransitionCancel(transition: Transition) { }override fun onTransitionStart(transition: Transition) { } })當窗口切換結束時,你添加到 enterTransition 中的 listener 被觸發,你可以在這里漸入 FAB 按鈕。為了實現這個效果,需要將 activity_detail.xml 中的 FAB 的 alpha 設置為 0:
android:alpha="0.0"Build & run!你會看到 FAB 的切入動畫更平滑了:
關于 action bar 和 navigation bar 的問題,首先要修改 styles.xml,將父主題設置為 Theme.AppCompat.Light.NoActionBar:
<style name="AppTheme" parent="Theme.AppCompat.Light.NoActionBar">因為在 styles.xml 中已經沒有 action bar 了,你需要用另外一個 xml 來定義它。
打開 activity_main.xml 在 LinearLayout 添加下列代碼,在 RecyclerView 標簽前面:
<include layout="@layout/toolbar" />這里直接在當前 layout 中包含了一個 toolbar layout,這是開始項目中已經提供的。現在你還要在 DetailActivity 的 layout 中做同樣的事情。
打開 activity_detail.xml 在第一個 FrameLayout 的底部,在包含的 LinearLayout 結束標簽之前添加:
<include layout="@layout/toolbar_detail"/>然后在 MainActivity 中,初始化 toolbar。在 onCreate() 方法底部添加:
setUpActionBar()這里,你將 findViewById 調用結果返回給新的 field,然后調用 setUpActionBar()。目前這只是一個空方法。接下來在 setUpActionBar() 中添加:
setSupportActionBar(toolbar)supportActionBar?.setDisplayHomeAsUpEnabled(false)supportActionBar?.setDisplayShowTitleEnabled(true)supportActionBar?.elevation = 7.0f這里,將你的 toolbar 設置為 action bar,設置 title 為可見,禁用 home 按鈕,然后通過 elevation 屬性添加一小點下陰影。
Build & run。你會看到沒什么改變,但這些修改已經打好了對 toolbar 進行動畫轉換的基礎。
打開 MainActivity,將 onItemClickListener 修改為:
private val onItemClickListener = object : TravelListAdapter.OnItemClickListener {override fun onItemClick(view: View, position: Int) {// 1val transitionIntent = DetailActivity.newIntent(this@MainActivity, position)val placeImage = view.findViewById<ImageView>(R.id.placeImage)val placeNameHolder = view.findViewById<LinearLayout>(R.id.placeNameHolder)// 2val navigationBar = findViewById<View>(android.R.id.navigationBarBackground)val statusBar = findViewById<View>(android.R.id.statusBarBackground)val imagePair = Pair.create(placeImage as View, "tImage")val holderPair = Pair.create(placeNameHolder as View, "tNameHolder")// 3val navPair = Pair.create(navigationBar, Window.NAVIGATION_BAR_BACKGROUND_TRANSITION_NAME)val statusPair = Pair.create(statusBar, Window.STATUS_BAR_BACKGROUND_TRANSITION_NAME)val toolbarPair = Pair.create(toolbar as View, "tActionBar")// 4val pairs = mutableListOf(imagePair, holderPair, statusPair, toolbarPair)if (navigationBar != null && navPair != null) {pairs += navPair}// 5val options = ActivityOptionsCompat.makeSceneTransitionAnimation(this@MainActivity,*pairs.toTypedArray())ActivityCompat.startActivity(this@MainActivity, transitionIntent, options.toBundle())} }和原來的方法不同,這個方法是:
很好!Build & run,你會看到動畫更平滑了:
現在如果你點擊 action/toolbar 或者導航欄下面的行,它不會在切換之前抖動了;它會用剩余的共享元素進行動畫轉換,這讓眼睛更加適應。切換到網格視圖,你會看到轉換動畫仍然運行良好。
這里是 app 最終效果:視頻
接下來做什么?
祝賀你:你編寫了一個全面使用 Android 材料設計的 app!要挑戰一下自己,請嘗試如下內容:
- 用 StaggeredLayoutMananger 實現 3 列的網格布局。
- 在 MainActivity 和 DetailActivity 中用不同的 palette 選項來提樣 Palette API。
- 在地名列表中添加一個按鈕,并把它作為共享元素的收藏按鈕,跳轉到 detail view。
- 讓轉換動畫效果更好——參考 Android 的 Newsstand app 是如何用呈現動畫從一個網格轉換到一個 detail view 的。要模擬這個效果的代碼你都知道。
- 嘗試創建你在這里實現的所有變形動畫,但這次用 animated-vector。
當然,請將這些技能用到你的 app 中,讓它們和這個 app 一樣棒!:]
要學習更多材料設計知識,請參考 Google 最新的 Google Design 網站。
你可以從這里下載完成后的項目。
請到下面或論壇中分享你的想法或者提問。
總結
以上是生活随笔為你收集整理的Android: Kotlin 材料设计入门的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: Web前端面试指导(四十四):什么是响应
- 下一篇: Vue3.0源码解读 - 响应式系统