標簽,然后將group的checkableBehavior屬性指定為single,group表示一個組,checkableBehavior指定為single表示組中的所有選單項只能單選,
這里一共定義了5個item,分別使用android:id屬性指定選單項的id, android:icon屬性指定選單項的圖示,android:title屬性指定選單項顯示的文字,就是這么簡單,現在我們已經把menu準備好了,
接下來,應該準備headerLayout了,這是一個可以隨意定制的布局,不過我并不想將它做得太復雜,這里簡單起見,我們就在headerLayout中放置頭像、用戶名、郵箱地址這3項內容吧,
說到頭像,還需要再準備一張圖片,并把它放在了drawable-xxhdpi目錄下,另外這張圖片最好是一張正方形圖片,因為待會我們會把它圓形化,然后右擊layout檔案夾→New→Layout resourcefile,創建一個nav_header.xml檔案,修改其中的代碼,如下所示:
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_
android:layout_height="180dp"
android:padding="10dp"
android:background="?attr/colorPrimary">
<de.hdodenhof.circleimageview.CircleImageView
android:layout_
android:layout_height="70dp"
android:id="@+id/icon_image"
android:src="https://www.cnblogs.com/1693977889zz/p/@drawable/nav_icon"
android:layout_centerInParent="true"/>
<TextView
android:layout_
android:layout_height="wrap_content"
android:id="@+id/mail"
android:layout_alignParentBottom="true"
android:text="[email protected]"
android:textColor="#FFF"
android:textSize="14sp"/>
<TextView
android:layout_
android:layout_height="wrap_content"
android:id="@+id/username"
android:layout_above="@id/mail"
android:text="Zhou zhou"
android:textColor="#FFF"
android:textSize="14sp"/>
</RelativeLayout>
布局檔案的最外層是一個RelativeLayout,將它的寬度設為match_parent,高度設為180dp,這是一個NavigationView比較適合的高度,然后指定它的背景色為colorPrimary,
在RelativeLayout中我們放置了3個控制元件,CircleImageView是一個用于將圖片圓形化的控制元件,它的用法基本和ImageView是完全一樣的,這里給它指定了一張圖片作為頭像,然后設定為居中顯示,另外兩個TextView分別用于顯示用戶名和郵箱地址,它們都用到了一些RelativeLayout的定位屬性,
現在menu和headerLayout都準備好了,我們終于可以使用NavigationView了,修改activity_main.xml中的代碼,如下所示:
<?xml version="1.0" encoding="utf-8"?>
<androidx.drawerlayout.widget.DrawerLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:id="@+id/drawer_layout"
android:layout_
android:layout_height="match_parent">
<FrameLayout
android:layout_
android:layout_height="match_parent">
<androidx.appcompat.widget.Toolbar
android:id="@+id/toolbar"
android:layout_
android:layout_height="?attr/actionBarSize"
android:background="?attr/colorPrimary"
android:theme="@style/ThemeOverlay.AppCompat.Dark.ActionBar"
android:popupTheme="@style/ThemeOverlay.AppCompat.Light"/>
</FrameLayout>
<com.google.android.material.navigation.NavigationView
android:layout_
android:layout_height="match_parent"
android:id="@+id/nav_view"
android:layout_gravity="start"
app:menu="@menu/nav_menu"
app:headerLayout="@layout/nav_header"/>
</androidx.drawerlayout.widget.DrawerLayout>
將之前的TextView換成了NavigationView,這樣滑動選單中顯示的內容也就變成NavigationView了,這里又通過app:menu和app:headerLayout屬性將我們剛才準備好的menu和headerLayout設定了進去,這樣NavigationView就定義完成了,
NavigationView雖然定義完成了,但是我們還要去處理選單項的點擊事件才行,修改MainActivity中的代碼,如下所示:
package com.zhouzhou.materialtest;
import androidx.annotation.NonNull;
import androidx.appcompat.app.ActionBar;
import androidx.appcompat.app.AppCompatActivity;
import androidx.appcompat.widget.Toolbar;
import androidx.core.view.GravityCompat;
import androidx.drawerlayout.widget.DrawerLayout;
import android.os.Bundle;
import android.view.Menu;
import android.view.MenuItem;
import android.widget.Toast;
import com.google.android.material.navigation.NavigationView;
public class MainActivity extends AppCompatActivity {
private DrawerLayout mDrawerLayout;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
Toolbar toolbar = (Toolbar) findViewById(R.id.toolbar);
setSupportActionBar(toolbar);
mDrawerLayout = (DrawerLayout) findViewById(R.id.drawer_layout);
NavigationView navigationView = (NavigationView) findViewById(R.id.nav_view);
ActionBar actionBar = getSupportActionBar();
if (actionBar != null) {
actionBar.setDisplayHomeAsUpEnabled(true);
actionBar.setHomeAsUpIndicator(R.drawable.ic_menu);
}
navigationView.setCheckedItem(R.id.nav_call);
navigationView.setNavigationItemSelectedListener(new NavigationView.OnNavigationItemSelectedListener() {
@Override
public boolean onNavigationItemSelected(@NonNull MenuItem item) {
mDrawerLayout.closeDrawers();
return true;
}
});
}
public boolean onCreateOptionsMenu(Menu menu) {
getMenuInflater().inflate(R.menu.toolbar,menu);
return true;
}
@Override
public boolean onOptionsItemSelected(@NonNull MenuItem item) {
switch (item.getItemId()) {
case R.id.backup:
Toast.makeText(this, "You clicked Backup", Toast.LENGTH_SHORT).show();
break;
case R.id.delete:
Toast.makeText(this, "You clicked Delete", Toast.LENGTH_SHORT).show();
break;
case R.id.settings:
Toast.makeText(this, "You clicked Settings", Toast.LENGTH_SHORT).show();
break;
case android.R.id.home:
mDrawerLayout.openDrawer(GravityCompat.START);
break;
default:
}
return true;
}
}
代碼還是比較簡單的,這里首先獲取到了NavigationView的實體,然后呼叫它的setCheckedItem()方法將Call選單項設定為默認選中,
接著呼叫了setNavigationItemSelectedListener()方法來設定一個選單項選中事件的監聽器,當用戶點擊了任意選單項時,就會回呼到onNavigationItemSelected()方法中,我們可以在這個方法中寫相應的邏輯處理,不過這里我并沒有附加任何邏輯,只是呼叫了DrawerLayout的closeDrawers()方法將滑動選單關閉,這也是合情合理的做法,
現在可以重新運行一下程式了,點擊一下Toolbar左側的導航按鈕,效果如圖:

MaterialDesign的魅力就在這里,它真的是一種非常美觀的設計理念,只要你按照它的各種規范和建議來設計界面,最終做出來的程式就是特別好看的,
12.4 懸浮按鈕和可互動提示
立面設計是Material Design中一條非常重要的設計思想,也就是說,按照Material Design的理念,應用程式的界面不僅僅只是一個平面,而應該是有立體效果的,在官方給出的示例中,最簡單且最具代表性的立面設計就是懸浮按鈕了,這種按鈕不屬于主界面平面的一部分,而是位于另外一個維度的,因此就會給人一種懸浮的感覺,
本節中我們會對這個懸浮按鈕的效果進行學習,另外還會學習一種可互動式的提示工具,關于提示工具,我們之前一直都是使用的Toast,但是Toast只能用于告知用戶某某事情已經發生了,用戶卻不能對此做出任何的回應,那么今天我們就將在這一方面進行擴展,
FloatingActionButton是Design Support庫中提供的一個控制元件,這個控制元件可以幫助我們比較輕松地實作懸浮按鈕的效果,它默認會使用colorAccent來作為按鈕的顏色,我們還可以通過給按鈕指定一個圖示來表明這個按鈕的作用是什么,
下面開始來具體實作,首先仍然需要提前準備好一個圖示,這里我放置了一張ic_done.png到drawable-xxhdpi目錄下,然后修改activity_main.xml中的代碼,如下所示:
<?xml version="1.0" encoding="utf-8"?>
<androidx.drawerlayout.widget.DrawerLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:id="@+id/drawer_layout"
android:layout_
android:layout_height="match_parent">
<FrameLayout
android:layout_
android:layout_height="match_parent">
<androidx.appcompat.widget.Toolbar
android:id="@+id/toolbar"
android:layout_
android:layout_height="?attr/actionBarSize"
android:background="?attr/colorPrimary"
android:theme="@style/ThemeOverlay.AppCompat.Dark.ActionBar"
android:popupTheme="@style/ThemeOverlay.AppCompat.Light"/>
</FrameLayout>
<com.google.android.material.floatingactionbutton.FloatingActionButton
android:layout_
android:layout_height="wrap_content"
android:id="@+id/fab"
android:layout_gravity="bottom|end"
android:layout_margin="16dp"
android:src="https://www.cnblogs.com/1693977889zz/p/@drawable/ic_done"/>
<com.google.android.material.navigation.NavigationView
android:layout_
android:layout_height="match_parent"
android:id="@+id/nav_view"
android:layout_gravity="start"
app:menu="@menu/nav_menu"
app:headerLayout="@layout/nav_header"/>
</androidx.drawerlayout.widget.DrawerLayout>
在主螢屏布局中加入了一個FloatingActionButton,
-
layout_width和layout_height屬性都指定成wrap_content,
-
layout_gravity屬性指定將這個控制元件放置于螢屏的右下角,
其中end的作業原理和之前的start是一樣的,即如果系統語言是從左往右的,那么end就表示在右邊,如果系統語言是從右往左的,那么end就表示在左邊,
-
layout_margin屬性給控制元件的四周留點邊距,緊貼著螢屏邊緣肯定是不好看的,
-
src屬性給FloatingActionButton設定了一個圖示,
就是這么簡單,現在我們就可以來運行一下了,效果如圖:

一個漂亮的懸浮按鈕就在螢屏的右下方出現了,
仔細觀察的話,會發現這個懸浮按鈕的下面還有一點陰影,因為FloatingActionButton是懸浮在當前界面上的,既然是懸浮,那么就理所應當會有投影,Design Support庫連這種細節都幫我們考慮到了,說到懸浮,其實我們還可以指定FloatingActionButton的懸浮高度,如下所示:
<com.google.android.material.floatingactionbutton.FloatingActionButton
android:layout_
android:layout_height="wrap_content"
android:id="@+id/fab"
android:layout_gravity="bottom|end"
android:layout_margin="16dp"
android:src="https://www.cnblogs.com/1693977889zz/p/@drawable/ic_done"
app:elevation="8dp"/>
使用app:elevation屬性來給FloatingActionButton指定一個高度值,高度值越大,投影范圍也越大,但是投影效果越淡,高度值越小,投影范圍也越小,但是投影效果越濃,當然這些效果的差異其實都不怎么明顯,我個人感覺使用默認的FloatingActionButton效果就已經足夠了,
接下來我們看一下FloatingActionButton是如何處理點擊事件的,畢竟,一個按鈕首先要能點擊才有意義,修改MainActivity中的代碼,如下所示:
public class MainActivity extends AppCompatActivity {
private DrawerLayout mDrawerLayout;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
...
FloatingActionButton fab = (FloatingActionButton) findViewById(R.id.fab);
fab.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View view) {
Toast.makeText(MainActivity.this, "FAB clicked", Toast.LENGTH_SHORT).show();
}
});
...
}
...
}
如果你在期待FloatingActionButton會有什么特殊用法的話,那可能就要讓你失望了,它和普通的Button其實沒什么兩樣,都是呼叫setOnClickListener()方法來注冊一個監聽器,當點擊按鈕時,就會執行監聽器中的onClick()方法,這里我們在onClick()方法中彈出了一個Toast,
現在重新運行一下程式,并點擊FloatingActionButton,效果如圖:

12.4.2 Snackbar
現在已經掌握了FloatingActionButton的基本用法,在上一小節處理點擊事件,仍然是使用Toast來作為提示工具的,本小節中就來學習一個Design Support庫提供的更加先進的提示工具——Snackbar,
首先要明確,Snackbar并不是Toast的替代品,它們兩者之間有著不同的應用場景,
- Toast的作用是告訴用戶現在發生了什么事情,但同時用戶只能被動接收這個事情,因為沒有什么辦法能讓用戶進行選擇,
- Snackbar則在這方面進行了擴展,它允許在提示當中加入一個可互動按鈕,當用戶點擊按鈕的時候可以執行一些額外的邏輯操作,
打個比方,如果我們在執行洗掉操作的時候只彈出一個Toast提示,那么用戶要是誤刪了某個重要資料的話肯定會十分抓狂吧,但是如果我們增加一個Undo按鈕,就相當于給用戶提供了一種彌補措施,從而大大降低了事故發生的概率,提升了用戶體驗,
Snackbar的用法也非常簡單,它和Toast是基本相似的,只不過可以額外增加一個按鈕的點擊事件,修改MainActivity中的代碼,如下所示:
public class MainActivity extends AppCompatActivity {
private DrawerLayout mDrawerLayout;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
...
FloatingActionButton fab = (FloatingActionButton) findViewById(R.id.fab);
fab.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View view) {
//Toast.makeText(MainActivity.this, "FAB clicked", Toast.LENGTH_SHORT).show();
Snackbar.make(view,"Data delete",Snackbar.LENGTH_SHORT).setAction("Undo", new View.OnClickListener() {
@Override
public void onClick(View view) {
Toast.makeText(MainActivity.this, "Data restored", Toast.LENGTH_SHORT).show();
}
}).show();
}
});
}
...
}
可以看到,這里呼叫了Snackbar的make()方法來創建一個Snackbar物件,make()方法的第一個引數需要傳入一個View,只要是當前界面布局的任意一個View都可以,Snackbar會使用這個View來自動查找最外層的布局,用于展示Snackbar,第二個引數就是Snackbar中顯示的內容,第三個引數是Snackbar顯示的時長,這些和Toast都是類似的,
接著這里又呼叫了一個setAction()方法來設定一個動作,從而讓Snackbar不僅僅是一個提示,而是可以和用戶進行互動的,簡單起見,我們在動作按鈕的點擊事件里面彈出一個Toast提示,最后呼叫show()方法讓Snackbar顯示出來,現在重新運行一下程式,并點擊懸浮按鈕,效果如圖:

可以看到,Snackbar從螢屏底部出現了,上面有我們所設定的提示文字,還有一個Undo按鈕,按鈕是可以點擊的,過一段時間后Snackbar會自動從螢屏底部消失,不管是出現還是消失,Snackbar都是帶有影片效果的,因此視覺體驗也會比較好,
不過,這個Snackbar竟然將我們的懸浮按鈕給遮擋住了,雖說也不是什么重大的問題,因為Snackbar過一會兒就會自動消失,但這種用戶體驗總歸是不友好的,只需要借助CoordinatorLayout就可以輕松解決,
12.4.3 CoordinatorLayout
CoordinatorLayout可以說是一個加強版的FrameLayout,這個布局也是由Design Support庫提供的,它在普通情況下的作用和FrameLayout基本一致,不過既然是Design Support庫中提供的布局,那么就必然有一些Material Design的魔力了,
事實上,CoordinatorLayout可以監聽其所有子控制元件的各種事件,然后自動幫助我們做出最為合理的回應,舉個簡單的例子,剛才彈出的Snackbar提示將懸浮按鈕遮擋住了,而如果我們能讓CoordinatorLayout監聽到Snackbar的彈出事件,那么它會自動將內部的FloatingActionButton向上偏移,從而確保不會被Snackbar遮擋到,
至于CoordinatorLayout的使用也非常簡單,只需要將原來的FrameLayout替換一下就可以了,修改activity_main.xml中的代碼,如下所示:
<?xml version="1.0" encoding="utf-8"?>
<androidx.drawerlayout.widget.DrawerLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:id="@+id/drawer_layout"
android:layout_
android:layout_height="match_parent">
<androidx.coordinatorlayout.widget.CoordinatorLayout
android:layout_
android:layout_height="match_parent">
<androidx.appcompat.widget.Toolbar
android:id="@+id/toolbar"
android:layout_
android:layout_height="?attr/actionBarSize"
android:background="?attr/colorPrimary"
android:theme="@style/ThemeOverlay.AppCompat.Dark.ActionBar"
android:popupTheme="@style/ThemeOverlay.AppCompat.Light"/>
<com.google.android.material.floatingactionbutton.FloatingActionButton
android:layout_
android:layout_height="wrap_content"
android:id="@+id/fab"
android:layout_gravity="bottom|end"
android:layout_margin="16dp"
android:src="https://www.cnblogs.com/1693977889zz/p/@drawable/ic_done"
app:elevation="8dp"/>
</androidx.coordinatorlayout.widget.CoordinatorLayout>
<com.google.android.material.navigation.NavigationView
android:layout_
android:layout_height="match_parent"
android:id="@+id/nav_view"
android:layout_gravity="start"
app:menu="@menu/nav_menu"
app:headerLayout="@layout/nav_header"/>
</androidx.drawerlayout.widget.DrawerLayout>
由于CoordinatorLayout本身就是一個加強版的FrameLayout,因此這種替換不會有任何的副作用,現在重新運行一下程式,并點擊懸浮按鈕,效果如圖:

可以看到,懸浮按鈕自動向上偏移了Snackbar的同等高度,從而確保不會被遮擋住,當Snackbar消失的時候,懸浮按鈕會自動向下偏移回到原來位置,
另外,懸浮按鈕的向上和向下偏移也是伴隨著影片效果的,且和Snackbar完全同步,整體效果看上去特別賞心悅目,不過我們回過頭來再思考一下,剛才說的是CoordinatorLayout可以監聽其所有子控制元件的各種事件,但是Snackbar好像并不是CoordinatorLayout的子控制元件吧,為什么它卻可以被監聽到呢?
還記得我們在Snackbar的make()方法中傳入的第一個引數嗎?這個引數就是用來指定Snackbar是基于哪個View來觸發的,剛才我們傳入的是FloatingActionButton本身,而FloatingActionButton是CoordinatorLayout中的子控制元件,因此這個事件就理所應當能被監聽到了,
你可以自己再做個試驗,如果給Snackbar的make()方法傳入一個DrawerLayout,那么Snackbar就會再次遮擋住懸浮按鈕,因為DrawerLayout不是CoordinatorLayout的子控制元件,CoordinatorLayout也就無法監聽到Snackbar的彈出和隱藏事件了,
12.5 卡片式布局
現在界面上最主要的一塊區域還處于空白狀態,這塊區域通常都是用來放置應用的主體內容的,準備使用一些精美的水果圖片來填充這部磁區域,
那么,為了要讓水果圖片也能Material化,本節中我們將會學習如何實作卡片式布局的效果,卡片式布局也是Materials Design中提出的一個新的概念,它可以讓頁面中的元素看起來就像在卡片中一樣,并且還能擁有圓角和投影,
12.5.1 CardView
CardView也是一個FrameLayout,只是額外提供了圓角和陰影等效果,看上去會有立體的感覺,先來看一下CardView的基本用法吧,如下所示:
<androidx.cardview.widget.CardView
android:layout_
android:layout_height="wrap_content"
app:cardCornerRadius="4dp"
app:cardElevation="5dp">
<TextView
android:layout_
android:layout_height="wrap_content"
android:id="@+id/info_text"/>
</androidx.cardview.widget.CardView>
這里定義了一個CardView布局,可以通過app:cardCornerRadius屬性指定卡片圓角的弧度,數值越大,圓角的弧度也越大,
另外,還可以通過app:cardElevation屬性指定卡片的高度,高度值越大,投影范圍也越大,但是投影效果越淡,高度值越小,投影范圍也越小,但是投影效果越濃,這一點和FloatingActionButton是一致的,
然后,我們在CardView布局中放置了一個TextView,那么這個TextView就會顯示在一張卡片當中了,CardView的用法就是這么簡單,但是我們顯然不可能在如此寬闊的一塊空白區域內只放置一張卡片,為了能夠充分利用螢屏的空間,這里使用RecyclerView來填充MaterialTest專案的主界面部分,
既然是要實作水果串列,那么首先肯定需要準備許多張水果圖片,這里我從網上挑選了一些精美的水果圖片,將它們復制到了專案當中,然后由于我們還需要用到RecyclerView、CardView這幾個控制元件,因此必須在app/build.gradle檔案中宣告這些庫的依賴才行:
dependencies {
implementation 'androidx.recyclerview:recyclerview:1.1.0'
implementation 'androidx.cardview:cardview:1.0.0'
implementation 'com.github.bumptech.glide:glide:4.13.0'
annotationProcessor 'com.github.bumptech.glide:compiler:4.13.0'
implementation "androidx.swiperefreshlayout:swiperefreshlayout:1.0.0"
}
注意上述宣告,這里添加了一個Glide庫的依賴,Glide是一個超級強大的圖片加載庫,它不僅可以用于加載本地圖片,還可以加載網路圖片、GIF圖片、甚至是本地視頻,
最重要的是,Glide的用法非常簡單,只需一行代碼就能輕松實作復雜的圖片加載功能,因此這里我們準備用它來加載水果圖片,Glide的專案主頁地址是:https://github.com/bumptech/glide,
接下來開始具體的代碼實作,修改activity_main.xml中的代碼,如下所示:
<?xml version="1.0" encoding="utf-8"?>
<androidx.drawerlayout.widget.DrawerLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:id="@+id/drawer_layout"
android:layout_
android:layout_height="match_parent">
<androidx.coordinatorlayout.widget.CoordinatorLayout
android:layout_
android:layout_height="match_parent">
<androidx.appcompat.widget.Toolbar
android:id="@+id/toolbar"
android:layout_
android:layout_height="?attr/actionBarSize"
android:background="?attr/colorPrimary"
android:theme="@style/ThemeOverlay.AppCompat.Dark.ActionBar"
android:popupTheme="@style/ThemeOverlay.AppCompat.Light"/>
<androidx.recyclerview.widget.RecyclerView
android:layout_
android:layout_height="match_parent"
android:id="@+id/recycler_view"/>
<com.google.android.material.floatingactionbutton.FloatingActionButton
android:layout_
android:layout_height="wrap_content"
android:id="@+id/fab"
android:layout_gravity="bottom|end"
android:layout_margin="16dp"
android:src="https://www.cnblogs.com/1693977889zz/p/@drawable/ic_done"
app:elevation="8dp"/>
</androidx.coordinatorlayout.widget.CoordinatorLayout>
<com.google.android.material.navigation.NavigationView
android:layout_
android:layout_height="match_parent"
android:id="@+id/nav_view"
android:layout_gravity="start"
app:menu="@menu/nav_menu"
app:headerLayout="@layout/nav_header"/>
</androidx.drawerlayout.widget.DrawerLayout>
在CoordinatorLayout中添加了一個RecyclerView,給它指定一個id,然后將寬度和高度都設定為match_parent,這樣RecyclerView也就占滿了整個布局的空間,接著定義一個物體類Fruit,代碼如下所示:
package com.zhouzhou.materialtest;
public class Fruit {
private String name;
private int imageId;
public Fruit(String name, int imageId) {
this.name = name;
this.imageId = imageId;
}
public String getName() {
return name;
}
public int getImageId() {
return imageId;
}
}
然后需要為RecyclerView的子項指定一個我們自定義的布局,在layout目錄下新建fruit_item. xml,代碼如下所示:
<?xml version="1.0" encoding="utf-8"?>
<androidx.cardview.widget.CardView
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_
android:layout_height="wrap_content"
android:layout_margin="5dp"
app:cardCornerRadius="4dp">
<LinearLayout
android:layout_
android:layout_height="wrap_content"
android:orientation="vertical">
<ImageView
android:layout_
android:layout_height="100dp"
android:id="@+id/fruit_image"
android:scaleType="centerCrop"/>
<TextView
android:layout_
android:layout_height="wrap_content"
android:id="@+id/fruit_name"
android:layout_gravity="center_horizontal"
android:layout_margin="5dp"
android:textSize="16sp"/>
</LinearLayout>
</androidx.cardview.widget.CardView>
這里使用了CardView來作為子項的最外層布局,從而使得RecyclerView中的每個元素都是在卡片當中的,CardView由于是一個FrameLayout,因此它沒有什么方便的定位方式,這里我們只好在CardView中再嵌套一個LinearLayout,然后在LinearLayout中放置具體的內容,
內容中,定義了一個ImageView用于顯示水果的圖片,又定義了一個TextView用于顯示水果的名稱,并讓TextView在水平方向上居中顯示,
注意:在ImageView中我們使用了一個scaleType屬性,這個屬性可以指定圖片的縮放模式,由于各張水果圖片的長寬比例可能都不一致,為了讓所有的圖片都能填充滿整個ImageView,這里使用了centerCrop模式,它可以讓圖片保持原有比例填充滿ImageView,并將超出螢屏的部分裁剪掉,
接下來,需要為RecyclerView準備一個配接器,新建FruitAdapter類,讓這個配接器繼承自RecyclerView.Adapter,并將泛型指定為FruitAdapter.ViewHolder,代碼如下所示:
package com.zhouzhou.materialtest;
import android.annotation.SuppressLint;
import android.content.Context;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.ImageView;
import android.widget.TextView;
import androidx.annotation.NonNull;
import androidx.cardview.widget.CardView;
import androidx.recyclerview.widget.RecyclerView;
import com.bumptech.glide.Glide;
import java.util.List;
public class FruitAdapter extends RecyclerView.Adapter<FruitAdapter.ViewHolder> {
private Context mContext;
private List<Fruit> mFruitList;
static class ViewHolder extends RecyclerView.ViewHolder{
CardView cardView;
ImageView fruitImage;
TextView fruitName;
public ViewHolder(@NonNull View itemView) {
super(itemView);
cardView = (CardView) itemView;
fruitImage = (ImageView) itemView.findViewById(R.id.fruit_image);
fruitName = (TextView) itemView.findViewById(R.id.fruit_name);
}
}
public FruitAdapter(List<Fruit> fruitList) {
mFruitList = fruitList;
}
@NonNull
@Override
public ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
if (mContext == null) {
mContext = parent.getContext();
}
@SuppressLint("ResourceType") View view = LayoutInflater.from(mContext).inflate(R.layout.fruit_item,parent,false);
return new ViewHolder(view);
}
@Override
public void onBindViewHolder(@NonNull ViewHolder holder, int position) {
Fruit fruit = mFruitList.get(position);
holder.fruitName.setText(fruit.getName());
Glide.with(mContext).load(fruit.getImageId()).into(holder.fruitImage);
}
@Override
public int getItemCount() {
return mFruitList.size();
}
}
上述代碼,和第3章中撰寫的FruitAdapter幾乎一樣,唯一需要注意的是,在onBindViewHolder()方法中我們使用了Glide來加載水果圖片,
Glide的用法實在是太簡單了,首先呼叫Glide.with()方法并傳入一個Context、Activity或Fragment引數,然后呼叫load()方法去加載圖片,可以是一個URL地址,也可以是一個本地路徑,或者是一個資源id,最后呼叫into()方法將圖片設定到具體某一個ImageView中就可以了,
那么我們為什么要使用Glide而不是傳統的設定圖片方式呢?
因為這次我從網上找的這些水果圖片像素都非常高,如果不進行壓縮就直接展示的話,很容易就會引起記憶體溢位,而使用Glide就完全不需要擔心這回事,因為Glide在內部做了許多非常復雜的邏輯操作,其中就包括了圖片壓縮,我們只需要安心按照Glide的標準用法去加載圖片就可以了,
這樣我們就將RecyclerView的配接器也準備好了,最后修改MainActivity中的代碼,如下所示:
package com.zhouzhou.materialtest;
import androidx.annotation.NonNull;
import androidx.appcompat.app.ActionBar;
import androidx.appcompat.app.AppCompatActivity;
import androidx.appcompat.widget.Toolbar;
import androidx.core.view.GravityCompat;
import androidx.drawerlayout.widget.DrawerLayout;
import androidx.recyclerview.widget.GridLayoutManager;
import androidx.recyclerview.widget.RecyclerView;
import android.os.Bundle;
import android.view.Menu;
import android.view.MenuItem;
import android.view.View;
import android.widget.Toast;
import com.google.android.material.floatingactionbutton.FloatingActionButton;
import com.google.android.material.navigation.NavigationView;
import com.google.android.material.snackbar.Snackbar;
import java.util.ArrayList;
import java.util.List;
import java.util.Random;
public class MainActivity extends AppCompatActivity {
private DrawerLayout mDrawerLayout;
private Fruit[] fruits = {new Fruit("Apple",R.drawable.apple),new Fruit("Banana",R.drawable.banana),
new Fruit("Orange",R.drawable.orange),new Fruit("Watermelon",R.drawable.watermelon),
new Fruit("Pear",R.drawable.pear),new Fruit("Grape",R.drawable.grape),
new Fruit("Pineapple",R.drawable.pineapple),new Fruit("Strawberry",R.drawable.strawberry),
new Fruit("Cherry",R.drawable.cherry),new Fruit("Mango",R.drawable.mango)};
private List<Fruit> fruitList = new ArrayList<>();
private FruitAdapter adapter;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
Toolbar toolbar = (Toolbar) findViewById(R.id.toolbar);
setSupportActionBar(toolbar);
mDrawerLayout = (DrawerLayout) findViewById(R.id.drawer_layout);
NavigationView navigationView = (NavigationView) findViewById(R.id.nav_view);
FloatingActionButton fab = (FloatingActionButton) findViewById(R.id.fab);
fab.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View view) {
//Toast.makeText(MainActivity.this, "FAB clicked", Toast.LENGTH_SHORT).show();
Snackbar.make(view,"Data delete",Snackbar.LENGTH_SHORT).setAction("Undo", new View.OnClickListener() {
@Override
public void onClick(View view) {
Toast.makeText(MainActivity.this, "Data restored", Toast.LENGTH_SHORT).show();
}
}).show();
}
});
ActionBar actionBar = getSupportActionBar();
if (actionBar != null) {
actionBar.setDisplayHomeAsUpEnabled(true);
actionBar.setHomeAsUpIndicator(R.drawable.ic_menu);
}
navigationView.setCheckedItem(R.id.nav_call);
navigationView.setNavigationItemSelectedListener(new NavigationView.OnNavigationItemSelectedListener() {
@Override
public boolean onNavigationItemSelected(@NonNull MenuItem item) {
mDrawerLayout.closeDrawers();
return true;
}
});
initFruits();
RecyclerView recyclerView = (RecyclerView) findViewById(R.id.recycler_view);
GridLayoutManager layoutManager = new GridLayoutManager(this,2);
recyclerView.setLayoutManager(layoutManager);
adapter = new FruitAdapter(fruitList);
recyclerView.setAdapter(adapter);
}
private void initFruits() {
fruitList.clear();
for (int i = 0; i < 50; i++) {
Random random = new Random();
int index = random.nextInt(fruits.length);
fruitList.add(fruits[index]);
}
}
public boolean onCreateOptionsMenu(Menu menu) {
getMenuInflater().inflate(R.menu.toolbar,menu);
return true;
}
@Override
public boolean onOptionsItemSelected(@NonNull MenuItem item) {
switch (item.getItemId()) {
case R.id.backup:
Toast.makeText(this, "You clicked Backup", Toast.LENGTH_SHORT).show();
break;
case R.id.delete:
Toast.makeText(this, "You clicked Delete", Toast.LENGTH_SHORT).show();
break;
case R.id.settings:
Toast.makeText(this, "You clicked Settings", Toast.LENGTH_SHORT).show();
break;
case android.R.id.home:
mDrawerLayout.openDrawer(GravityCompat.START);
break;
default:
}
return true;
}
}
在MainActivity中首先定義了一個陣列,陣列里面存放了很多個Fruit的實體,每個實體都代表著一種水果,然后在initFruits()方法中,先是清空了一下fruitList中的資料,接著使用一個隨機函式,從剛才定義的Fruit陣列中隨機挑選一個水果放入到fruitList當中,這樣每次打開程式看到的水果資料都會是不同的,另外,為了讓界面上的資料多一些,這里使用了一個回圈,隨機挑選50個水果,
之后的用法就是RecyclerView的標準用法了,不過這里使用了GridLayoutManager這種布局方式,在第3章中我們已經學過了LinearLayoutManager和StaggeredGridLayoutManager,現在終于將所有的布局方式都補齊了,GridLayoutManager的用法也沒有什么特別之處,它的建構式接收兩個引數,第一個是Context,第二個是列數,這里我們希望每一行中會有兩列資料,現在重新運行一下程式,效果如圖:

水果圖片成功展示出來了,每個水果都是在一張單獨的卡片當中的,并且還擁有圓角和投影,另外,由于我們是使用隨機的方式來獲取水果資料的,因此界面上會有一些重復的水果出現,這屬于正常現象,
toolbar怎么不見了!是被RecyclerView給擋住了,解決需要借助到另外一個工具——AppBarLayout,
12.5.2 AppBarLayout
分析一下為什么RecyclerView會把Toolbar給遮擋住?
由于RecyclerView和Toolbar都是放置在CoordinatorLayout中的,而前面已經說過,CoordinatorLayout就是一個加強版的FrameLayout,那么FrameLayout中的所有控制元件在不進行明確定位的情況下,默認都會擺放在布局的左上角,從而也就產生了遮擋的現象,
那么該如何解決呢?傳統情況下,使用偏移是唯一的解決辦法,即讓RecyclerView向下偏移一個Toolbar的高度,從而保證不會遮擋到Toolbar,不過我們使用的并不是普通的FrameLayout,而是CoordinatorLayout,因此自然會有一些更加巧妙的解決辦法,
這里準備使用Design Support庫中提供的另外一個工具——AppBarLayout,AppBarLayout實際上是一個垂直方向的LinearLayout,它在內部做了很多滾動事件的封裝,并應用了一些Material Design的設計理念,
那么我們怎樣使用AppBarLayout才能解決前面的覆寫問題呢?只需要兩步:第一步將Toolbar嵌套到AppBarLayout中,第二步給RecyclerView指定一個布局行為,修改activity_main.xml中的代碼,如下所示:
<?xml version="1.0" encoding="utf-8"?>
<androidx.drawerlayout.widget.DrawerLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:id="@+id/drawer_layout"
android:layout_
android:layout_height="match_parent">
<androidx.coordinatorlayout.widget.CoordinatorLayout
android:layout_
android:layout_height="match_parent">
<com.google.android.material.appbar.AppBarLayout
android:layout_
android:layout_height="wrap_content">
<androidx.appcompat.widget.Toolbar
android:id="@+id/toolbar"
android:layout_
android:layout_height="?attr/actionBarSize"
android:background="?attr/colorPrimary"
android:theme="@style/ThemeOverlay.AppCompat.Dark.ActionBar"
android:popupTheme="@style/ThemeOverlay.AppCompat.Light"/>
</com.google.android.material.appbar.AppBarLayout>
<androidx.recyclerview.widget.RecyclerView
android:layout_
android:layout_height="match_parent"
android:id="@+id/recycler_view"
app:layout_behavior="@string/appbar_scrolling_view_behavior"/>
<com.google.android.material.floatingactionbutton.FloatingActionButton
android:layout_
android:layout_height="wrap_content"
android:id="@+id/fab"
android:layout_gravity="bottom|end"
android:layout_margin="16dp"
android:src="https://www.cnblogs.com/1693977889zz/p/@drawable/ic_done"
app:elevation="8dp"/>
</androidx.coordinatorlayout.widget.CoordinatorLayout>
<com.google.android.material.navigation.NavigationView
android:layout_
android:layout_height="match_parent"
android:id="@+id/nav_view"
android:layout_gravity="start"
app:menu="@menu/nav_menu"
app:headerLayout="@layout/nav_header"/>
</androidx.drawerlayout.widget.DrawerLayout>
布局檔案并沒有什么太大的變化,首先定義了一個AppBarLayout,并將Toolbar放置在了AppBarLayout里面,然后在RecyclerView中使用app:layout_behavior屬性指定了一個布局行為,其中appbar_scrolling_view_behavior這個字串也是由Design Support庫提供的,現在重新運行一下程式,如圖:

雖說使用AppBarLayout已經成功解決了RecyclerView遮擋Toolbar的問題,但是剛才有提到過,說AppBarLayout中應用了一些Material Design的設計理念,好像從上面的例子完全體現不出來,
事實上,當RecyclerView滾動的時候就已經將滾動事件都通知給AppBarLayout了,只是我們還沒進行處理而已,那么下面就讓我們來進一步優化,看看AppBarLayout到底能實作什么樣的Material Design效果,
當AppBarLayout接收到滾動事件的時候,它內部的子控制元件其實是可以指定如何去影響這些事件的,通過app:layout_scrollFlags屬性就能實作,修改activity_main.xml中的代碼,如下所示:
<?xml version="1.0" encoding="utf-8"?>
<androidx.drawerlayout.widget.DrawerLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:id="@+id/drawer_layout"
android:layout_
android:layout_height="match_parent">
<androidx.coordinatorlayout.widget.CoordinatorLayout
android:layout_
android:layout_height="match_parent">
<com.google.android.material.appbar.AppBarLayout
android:layout_
android:layout_height="wrap_content">
<androidx.appcompat.widget.Toolbar
android:id="@+id/toolbar"
android:layout_
android:layout_height="?attr/actionBarSize"
android:background="?attr/colorPrimary"
android:theme="@style/ThemeOverlay.AppCompat.Dark.ActionBar"
android:popupTheme="@style/ThemeOverlay.AppCompat.Light"
app:layout_scrollFlags="scroll|enterAlways|snap"/>
</com.google.android.material.appbar.AppBarLayout>
...
</androidx.coordinatorlayout.widget.CoordinatorLayout>
...
</androidx.drawerlayout.widget.DrawerLayout>
Toolbar中添加了一個app:layout_scrollFlags屬性,屬性的值指定成了scroll|enterAlways|snap:
- scroll表示當RecyclerView向上滾動的時候,Toolbar會跟著一起向上滾動并實作隱藏;
- enterAlways表示當RecyclerView向下滾動的時候,Toolbar會跟著一起向下滾動并重新顯示;
- snap表示當Toolbar還沒有完全隱藏或顯示的時候,會根據當前滾動的距離,自動選擇是隱藏還是顯示,
改動的就只有這一行代碼而已,現在重新運行一下程式,并向上滾動RecyclerView,效果如圖:

可以看到,隨著我們向上滾動RecyclerView, Toolbar竟然消失了,而向下滾動RecyclerView, Toolbar又會重新出現,這其實也是Material Design中的一項重要設計思想,因為當用戶在向上滾動RecyclerView的時候,其注意力肯定是在RecyclerView的內容上面的,這個時候如果Toolbar還占據著螢屏空間,就會在一定程度上影響用戶的閱讀體驗,而將Toolbar隱藏則可以讓閱讀體驗達到最佳狀態,當用戶需要操作Toolbar上的功能時,只需要輕微向下滾動,Toolbar就會重新出現,
這種設計方式,既保證了用戶的最佳閱讀效果,又不影響任何功能上的操作,Material Design考慮得就是這么細致入微,當然了,像這種功能,如果是使用ActionBar的話,那就完全不可能實作了,Toolbar的出現為我們提供了更多的可能,
12.6 下拉重繪
下拉重繪這種功能早就不是什么新鮮的東西了,幾乎所有的應用里都會有這個功能,不過市面上現有的下拉重繪功能在風格上都各不相同,并且和MaterialDesign還有些格格不入的感覺,因此,谷歌為了讓Android的下拉重繪風格能有一個統一的標準,于是在Material Design中制定了一個官方的設計規范,當然,我們并不需要去深入了解這個規范到底是什么樣的,因為谷歌早就提供好了現成的控制元件,我們只需要在專案中直接使用就可以了,
SwipeRefreshLayout就是用于實作下拉重繪功能的核心類,我們把想要實作下拉重繪功能的控制元件放置到SwipeRefreshLayout中,就可以迅速讓這個控制元件支持下拉重繪,那么在MaterialTest專案中,應該支持下拉重繪功能的控制元件自然就是RecyclerView了,
由于SwipeRefreshLayout的用法也比較簡單,下面我們就直接開始使用了,修改activity_main.xml中的代碼,如下所示:
<androidx.swiperefreshlayout.widget.SwipeRefreshLayout
android:layout_
android:layout_height="match_parent"
android:id="@+id/swipe_refresh"
app:layout_behavior="@string/appbar_scrolling_view_behavior">
<androidx.recyclerview.widget.RecyclerView
android:layout_
android:layout_height="match_parent"
android:id="@+id/recycler_view"/>
</androidx.swiperefreshlayout.widget.SwipeRefreshLayout>

在RecyclerView的外面又嵌套了一層SwipeRefreshLayout,這樣RecyclerView就自動擁有下拉重繪功能了,另外需要注意,由于RecyclerView現在變成了SwipeRefreshLayout的子控制元件,因此之前使用app:layout_behavior宣告的布局行為現在也要移到SwipeRefreshLayout中才行,
不過這還沒有結束,雖然RecyclerView已經支持下拉重繪功能了,但是我們還要在代碼中處理具體的重繪邏輯才行,修改MainActivity中的代碼,如下所示:
public class MainActivity extends AppCompatActivity {
...
private SwipeRefreshLayout swipeRefreshLayout;
...
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
...
swipeRefreshLayout = (SwipeRefreshLayout) findViewById(R.id.swipe_refresh);
swipeRefreshLayout.setColorSchemeColors(com.google.android.material.R.color.design_default_color_primary);
swipeRefreshLayout.setOnRefreshListener(new SwipeRefreshLayout.OnRefreshListener() {
@Override
public void onRefresh() {
refreshFruits();
}
});
}
private void refreshFruits() {
new Thread(new Runnable() {
@Override
public void run() {
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
runOnUiThread(new Runnable() {
@Override
public void run() {
initFruits();
adapter.notifyDataSetChanged();
swipeRefreshLayout.setRefreshing(false);
}
});
}
}).start();
}
...
}
首先通過findViewById()方法拿到SwipeRefreshLayout的實體,然后呼叫setColorSchemeResources()方法來設定下拉重繪進度條的顏色,這里就使用主題中的colorPrimary作為進度條的顏色了,接著呼叫setOnRefreshListener()方法來設定一個下拉重繪的監聽器,當觸發了下拉重繪操作的時候就會回呼這個監聽器的onRefresh()方法,然后我們在這里去處理具體的重繪邏輯就可以了,
通常情況下,onRefresh()方法中應該是去網路上請求最新的資料,然后再將這些資料展示出來,這里簡單起見,我們就不和網路進行互動了,而是呼叫一個refreshFruits()方法進行本地重繪操作,refreshFruits()方法中先是開啟了一個執行緒,然后將執行緒沉睡兩秒鐘,之所以這么做,是因為本地重繪操作速度非常快,如果不將執行緒沉睡的話,重繪立刻就結束了,從而看不到重繪的程序,沉睡結束之后,這里使用了runOnUiThread()方法將執行緒切換回主執行緒,然后呼叫initFruits()方法重新生成資料,接著再呼叫FruitAdapter的notifyDataSetChanged()方法通知資料發生了變化,最后呼叫SwipeRefreshLayout的setRefreshing()方法并傳入false,用于表示重繪事件結束,并隱藏重繪進度條,
現在可以重新運行一下程式了,在螢屏的主界面向下拖動,會有一個下拉重繪的進度條出現,松手后就會自動進行重繪了,效果如圖:

下拉重繪的進度條只會停留兩秒鐘,之后就會自動消失,界面上的水果資料也會隨之更新,這樣我們就把下拉重繪的功能也成功實作了,
12.7 可折疊式標題欄
雖說我們現在的標題欄是使用Toolbar來撰寫的,不過它看上去和傳統的ActionBar其實沒什么兩樣,只不過可以回應RecyclerView的滾動事件來進行隱藏和顯示,而Material Design中并沒有限定標題欄必須是長這個樣子的,事實上,我們可以根據自己的喜好隨意定制標題欄的樣式,那么本節中我們就來實作一個可折疊式標題欄的效果,需要借助CollapsingToolbarLayout這個工具,
CollapsingToolbarLayout是一個作用于Toolbar基礎之上的布局,它也是由Design Support庫提供的,CollapsingToolbarLayout可以讓Toolbar的效果變得更加豐富,不僅僅是展示一個標題欄,而是能夠實作非常華麗的效果,
不過,CollapsingToolbarLayout是不能獨立存在的,它在設計的時候就被限定只能作為AppBarLayout的直接子布局來使用,而AppBarLayout又必須是CoordinatorLayout的子布局,因此本節中我們要實作的功能其實需要綜合運用前面所學的各種知識,
首先,需要一個額外的活動來作為水果的詳情展示界面,右擊com.zhouzhou.materialtest包→New→Activity→Empty Activity,創建一個FruitActivity,并將布局名指定成activity_fruit.xml,然后我們開始撰寫水果詳情展示界面的布局,
由于整個布局檔案比較復雜,這里我準備采用分段撰寫的方式,activity_fruit.xml中的內容主要分為兩部分,一個是水果標題欄,一個是水果內容詳情,我們來一步步實作,首先實作標題欄部分,這里使用CoordinatorLayout來作為最外層布局,如下所示:
<?xml version="1.0" encoding="utf-8"?>
<androidx.coordinatorlayout.widget.CoordinatorLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_
android:layout_height="match_parent">
</androidx.coordinatorlayout.widget.CoordinatorLayout>
注意:始終記得要定義一個xmlns:app的命名空間,在Material Design的開發中會經常用到它,接著我們在CoordinatorLayout中嵌套一個AppBarLayout,如下所示:
<?xml version="1.0" encoding="utf-8"?>
<androidx.coordinatorlayout.widget.CoordinatorLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_
android:layout_height="match_parent">
<com.google.android.material.appbar.AppBarLayout
android:layout_
android:layout_height="250dp"
android:id="@+id/appBar">
</com.google.android.material.appbar.AppBarLayout>
</androidx.coordinatorlayout.widget.CoordinatorLayout>
給AppBarLayout定義一個id,將它的寬度指定為match_parent,高度指定為250dp,接下來在AppBarLayout中再嵌套一個CollapsingToolbarLayout,如下所示:
<?xml version="1.0" encoding="utf-8"?>
<androidx.coordinatorlayout.widget.CoordinatorLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_
android:layout_height="match_parent">
<com.google.android.material.appbar.AppBarLayout
android:layout_
android:layout_height="250dp"
android:id="@+id/appBar">
<com.google.android.material.appbar.CollapsingToolbarLayout
android:layout_
android:layout_height="match_parent"
android:id="@+id/collapsing_toolbar"
android:theme="@style/ThemeOverlay.AppCompat.Dark.ActionBar"
app:contentScrim="?attr/colorPrimary"
app:layout_scrollFlags="scroll|exitUntilCollapsed">
</com.google.android.material.appbar.CollapsingToolbarLayout>
</com.google.android.material.appbar.AppBarLayout>
</androidx.coordinatorlayout.widget.CoordinatorLayout>
這里使用了新的布局CollapsingToolbarLayout:
- android:theme屬性指定了一個ThemeOverlay.AppCompat.Dark.ActionBar的主題,之前在activity_main.xml中給Toolbar指定的也是這個主題,只不過這里要實作更加高級的Toolbar效果,因此需要將這個主題的指定提到上一層來,
- app:contentScrim屬性用于指定CollapsingToolbarLayout在趨于折疊狀態以及折疊之后的背景色,其實CollapsingToolbarLayout在折疊之后就是一個普通的Toolbar,那么背景色肯定應該是colorPrimary了,
- app:layout_scrollFlags屬性,之前是給Toolbar指定的,現在也移到外面來了,
- scroll表示CollapsingToolbarLayout會隨著水果內容詳情的滾動一起滾動;
- exitUntilCollapsed表示當CollapsingToolbarLayout隨著滾動完成折疊之后就保留在界面上,不再移出螢屏,
接下來,我們在CollapsingToolbarLayout中定義標題欄的具體內容,如下所示:
<?xml version="1.0" encoding="utf-8"?>
<androidx.coordinatorlayout.widget.CoordinatorLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_
android:layout_height="match_parent">
<com.google.android.material.appbar.AppBarLayout
android:layout_
android:layout_height="250dp"
android:id="@+id/appBar">
<com.google.android.material.appbar.CollapsingToolbarLayout
android:layout_
android:layout_height="match_parent"
android:id="@+id/collapsing_toolbar"
android:theme="@style/ThemeOverlay.AppCompat.Dark.ActionBar"
app:contentScrim="?attr/colorPrimary"
app:layout_scrollFlags="scroll|exitUntilCollapsed">
<ImageView
android:layout_
android:layout_height="match_parent"
android:id="@+id/fruit_image_view"
android:scaleType="centerCrop"
app:layout_collapseMode="parallax"/>
<androidx.appcompat.widget.Toolbar
android:layout_
android:layout_height="?attr/actionBarSize"
android:id="@+id/toolbar"
app:layout_collapseMode="pin"/>
</com.google.android.material.appbar.CollapsingToolbarLayout>
</com.google.android.material.appbar.AppBarLayout>
</androidx.coordinatorlayout.widget.CoordinatorLayout>
可以看到,我們在CollapsingToolbarLayout中定義了一個ImageView和一個Toolbar,也就意味著,這個高級版的標題欄將是由普通的標題欄加上圖片組合而成的,
- app:layout_collapseMode它用于指定當前控制元件在CollapsingToolbarLayout折疊程序中的折疊模式,
- Toolbar指定成pin,表示在折疊的程序中位置始終保持不變;
ImageView指定成parallax,表示會在折疊的程序中產生一定的錯位偏移,這種模式的視覺效果會非常好,
這樣我們就將水果標題欄的界面撰寫完成了,下面開始撰寫水果內容詳情部分,繼續修改activity_fruit.xml中的代碼,如下所示:
<?xml version="1.0" encoding="utf-8"?>
<androidx.coordinatorlayout.widget.CoordinatorLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_
android:layout_height="match_parent">
<com.google.android.material.appbar.AppBarLayout
android:layout_
android:layout_height="250dp"
android:id="@+id/appBar">
<com.google.android.material.appbar.CollapsingToolbarLayout
android:layout_
android:layout_height="match_parent"
android:id="@+id/collapsing_toolbar"
android:theme="@style/ThemeOverlay.AppCompat.Dark.ActionBar"
app:contentScrim="?attr/colorPrimary"
app:layout_scrollFlags="scroll|exitUntilCollapsed">
<ImageView
android:layout_
android:layout_height="match_parent"
android:id="@+id/fruit_image_view"
android:scaleType="centerCrop"
app:layout_collapseMode="parallax"/>
<androidx.appcompat.widget.Toolbar
android:layout_
android:layout_height="?attr/actionBarSize"
android:id="@+id/toolbar"
app:layout_collapseMode="pin"/>
</com.google.android.material.appbar.CollapsingToolbarLayout>
</com.google.android.material.appbar.AppBarLayout>
<androidx.core.widget.NestedScrollView
android:layout_
android:layout_height="match_parent"
app:layout_behavior="@string/appbar_scrolling_view_behavior">
</androidx.core.widget.NestedScrollView>
</androidx.coordinatorlayout.widget.CoordinatorLayout>
水果內容詳情的最外層布局使用了一個NestedScrollView,注意它和AppBarLayout是平級的,我們之前在9.2.1小節學過ScrollView的用法,它允許使用滾動的方式來查看螢屏以外的資料,而NestedScrollView在此基礎之上還增加了嵌套回應滾動事件的功能,
由于CoordinatorLayout本身已經可以回應滾動事件了,因此我們在它的內部就需要使用NestedScrollView或RecyclerView這樣的布局,另外,這里還通過app:layout_behavior屬性指定了一個布局行為,這和之前在RecyclerView中的用法是一模一樣的,不管是ScrollView還是NestedScrollView,它們的內部都只允許存在一個直接子布局,因此,如果我們想要在里面放入很多東西的話,通常都會先嵌套一個LinearLayout,然后再在LinearLayout中放入具體的內容就可以了,如下所示:
<?xml version="1.0" encoding="utf-8"?>
<androidx.coordinatorlayout.widget.CoordinatorLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_
android:layout_height="match_parent">
...
<androidx.core.widget.NestedScrollView
android:layout_
android:layout_height="match_parent"
app:layout_behavior="@string/appbar_scrolling_view_behavior">
<LinearLayout
android:layout_
android:layout_height="wrap_content"
android:orientation="vertical">
</LinearLayout>
</androidx.core.widget.NestedScrollView>
</androidx.coordinatorlayout.widget.CoordinatorLayout>
這里我們嵌套了一個垂直方向的LinearLayout,并將layout_width設定為match_parent,將layout_height設定為wrap_content,接下來在LinearLayout中放入具體的內容,這里準備使用一個TextView來顯示水果的內容詳情,并將TextView放在一個卡片式布局當中,如下所示:
<?xml version="1.0" encoding="utf-8"?>
<androidx.coordinatorlayout.widget.CoordinatorLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_
android:layout_height="match_parent">
...
<androidx.core.widget.NestedScrollView
android:layout_
android:layout_height="match_parent"
app:layout_behavior="@string/appbar_scrolling_view_behavior">
<LinearLayout
android:layout_
android:layout_height="wrap_content"
android:orientation="vertical">
<androidx.cardview.widget.CardView
android:layout_
android:layout_height="wrap_content"
android:layout_marginBottom="15dp"
android:layout_marginLeft="15dp"
android:layout_marginRight="15dp"
android:layout_marginTop="35dp"
app:cardCornerRadius="4dp">
<TextView
android:layout_
android:layout_height="wrap_content"
android:id="@+id/fruit_content_text"
android:layout_margin="10dp"/>
</androidx.cardview.widget.CardView>
</LinearLayout>
</androidx.core.widget.NestedScrollView>
</androidx.coordinatorlayout.widget.CoordinatorLayout>
需要注意的是,這里為了讓界面更加美觀,我在CardView和TextView上都加了一些邊距,其中,CardView的marginTop加了35dp的邊距,這是為下面要撰寫的東西留出空間,
這樣就把水果標題欄和水果內容詳情的界面都撰寫完了,不過我們還可以在界面上再添加一個懸浮按鈕,這個懸浮按鈕并不是必需的,根據具體的需求添加就可以了,如果加入的話,我們將免費獲得一些額外的影片效果,
為了做出示范,我就準備在activity_fruit.xml中加入一個懸浮按鈕了,這個界面是一個水果詳情展示界面,那么我就加入一個表示評論作用的懸浮按鈕吧,首先需要提前準備好一個圖示,這里我放置了一張ic_comment.png到drawable-xxhdpi目錄下,然后修改activity_fruit.xml中的代碼,如下所示:
<?xml version="1.0" encoding="utf-8"?>
<androidx.coordinatorlayout.widget.CoordinatorLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_
android:layout_height="match_parent">
<com.google.android.material.appbar.AppBarLayout
android:layout_
android:layout_height="250dp"
android:id="@+id/appBar">
...
</com.google.android.material.appbar.AppBarLayout>
<androidx.core.widget.NestedScrollView
android:layout_
android:layout_height="match_parent"
app:layout_behavior="@string/appbar_scrolling_view_behavior">
...
</androidx.core.widget.NestedScrollView>
<com.google.android.material.floatingactionbutton.FloatingActionButton
android:layout_
android:layout_height="wrap_content"
android:layout_margin="16dp"
android:src="https://www.cnblogs.com/1693977889zz/p/@drawable/ic_comment"
app:layout_anchor="@id/appBar"
app:layout_anchorGravity="bottom|end"/>
</androidx.coordinatorlayout.widget.CoordinatorLayout>
這里加入了一個FloatingActionButton,它和AppBarLayout以及NestedScrollView是平級的,FloatingActionButton中使用app:layout_anchor屬性指定了一個錨點,我們將錨點設定為AppBarLayout,這樣懸浮按鈕就會出現在水果標題欄的區域內,接著又使用app:layout_anchorGravity屬性將懸浮按鈕定位在標題欄區域的右下角,
現在我們終于將整個activity_fruit.xml布局都撰寫完了,內容雖然比較長,但由于是分段撰寫的,界面完成了之后,接下來我們開始撰寫功能邏輯,修改FruitActivity中的代碼,如下所示:
package com.zhouzhou.materialtest;
import androidx.annotation.NonNull;
import androidx.appcompat.app.ActionBar;
import androidx.appcompat.app.AppCompatActivity;
import androidx.appcompat.widget.Toolbar;
import android.content.Intent;
import android.os.Bundle;
import android.view.MenuItem;
import android.widget.ImageView;
import android.widget.TextView;
import com.bumptech.glide.Glide;
import com.google.android.material.appbar.CollapsingToolbarLayout;
public class FruitActivity extends AppCompatActivity {
private static final String FRUIT_NAME = "fruit_name";
private static final String FRUIT_IMAGE_ID = "fruit_image_id";
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_fruit);
Intent intent = getIntent();
String fruitName = intent.getStringExtra(FRUIT_NAME);
int fruitImageId = intent.getIntExtra(FRUIT_IMAGE_ID,0);
Toolbar toolbar = (Toolbar) findViewById(R.id.toolbar);
CollapsingToolbarLayout collapsingToolbarLayout = (CollapsingToolbarLayout) findViewById(R.id.collapsing_toolbar);
ImageView fruitImageView = (ImageView) findViewById(R.id.fruit_image_view);
TextView fruitContentText = (TextView) findViewById(R.id.fruit_content_text);
setSupportActionBar(toolbar);
ActionBar actionBar = getSupportActionBar();
if (actionBar != null) {
actionBar.setDisplayHomeAsUpEnabled(true);
}
collapsingToolbarLayout.setTitle(fruitName);
Glide.with(this).load(fruitImageId).into(fruitImageView);
String fruitContent = generateFruit(fruitName);
fruitContentText.setText(fruitContent);
}
private String generateFruit(String fruitName) {
StringBuilder fruitContent = new StringBuilder();
for (int i = 0; i < 500; i++) {
fruitContent.append(fruitName);
}
return fruitContent.toString();
}
@Override
public boolean onOptionsItemSelected(@NonNull MenuItem item) {
switch (item.getItemId()) {
case android.R.id.home:
finish();
return true;
}
return super.onOptionsItemSelected(item);
}
}
FruitActivity中的代碼并不是很復雜,首先,在onCreate()方法中,通過Intent獲取到傳入的水果名和水果圖片的資源id,然后通過findViewById()方法拿到剛才在布局檔案中定義的各個控制元件的實體,接著就是使用了Toolbar的標準用法,將它作為ActionBar顯示,并啟用HomeAsUp按鈕,
由于HomeAsUp按鈕的默認圖示就是一個回傳箭頭,這正是我們所期望的,因此就不用再額外設定別的圖示了,接下來開始填充界面上的內容,呼叫CollapsingToolbarLayout的setTitle()方法將水果名設定成當前界面的標題,然后使用Glide加載傳入的水果圖片,并設定到標題欄的ImageView上面,接著需要填充水果的內容詳情,由于這只是一個示例程式,并不需要什么真實的資料,所以我使用了一個generateFruitContent()方法將水果名回圈拼接500次,從而生成了一個比較長的字串,將它設定到了TextView上面,
最后,我們在onOptionsItemSelected()方法中處理了HomeAsUp按鈕的點擊事件,當點擊了這個按鈕時,就呼叫finish()方法關閉當前的活動,從而回傳上一個活動,
還差最關鍵的一步,就是處理RecyclerView的點擊事件,不然的話我們根本就無法打開FruitActivity,修改FruitAdapter中的代碼,如下所示:
public class FruitAdapter extends RecyclerView.Adapter<FruitAdapter.ViewHolder> {
...
@NonNull
@Override
public ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
if (mContext == null) {
mContext = parent.getContext();
}
@SuppressLint("ResourceType") View view = LayoutInflater.from(mContext).inflate(R.layout.fruit_item,parent,false);
final ViewHolder holder = new ViewHolder(view);
holder.cardView.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View view) {
int position = holder.getAdapterPosition();
Fruit fruit = mFruitList.get(position);
Intent intent = new Intent(mContext,FruitActivity.class);
intent.putExtra(FruitActivity.FRUIT_NAME,fruit.getName());
intent.putExtra(FruitActivity.FRUIT_IMAGE_ID,fruit.getImageId());
mContext.startActivity(intent);
}
});
return holder;
}
...
}
最關鍵的一步其實也是最簡單的,這里我們給CardView注冊了一個點擊事件監聽器,然后在點擊事件中獲取當前點擊項的水果名和水果圖片資源id,把它們傳入到Intent中,最后呼叫startActivity()方法啟動FruitActivity,
現在重新運行一下程式,并點擊界面上的任意一個水果,比如我點擊了橘子,效果如圖:

這個界面上的內容分為三部分,水果標題欄、水果內容詳情和懸浮按鈕,
Toolbar和水果背景圖完美地融合到了一起,既保證了圖片的展示空間,又不影響Toolbar的任何功能,那個向左的箭頭就是用來回傳上一個活動的,當嘗試向上拖動水果內容詳情,會發現水果背景圖上的標題會慢慢縮小,并且背景圖會產生一些錯位偏移的效果:

這是由于用戶想要查看水果的內容詳情,此時界面的重點在具體的內容上面,因此標題欄就會自動進行折疊,從而節省螢屏空間,繼續向上拖動,直到標題欄變成完全折疊狀態,效果如圖:

可以看到,標題欄的背景圖片不見了,懸浮按鈕也自動消失了,現在水果標題欄變成了一個最普通的Toolbar,這是由于用戶正在閱讀具體的內容,我們需要給他們提供最充分的閱讀空間,而如果這個時候向下拖動水果內容詳情,就會執行一個完全相反的影片程序,最侄訓復開始時的界面效果,
12.7.2 充分利用系統狀態欄空間
觀察一下,你會發現水果的背景圖片和系統的狀態欄總有一些不搭的感覺,如果我們能將背景圖和狀態欄融合到一起,那這個視覺體驗絕對能提升好幾個檔次,
只不過很可惜的是,在Android 5.0系統之前,我們是無法對狀態欄的背景或顏色進行操作的,那個時候也還沒有Material Design的概念,但是Android5.0及之后的系統都是支持這個功能的,因此這里我們就來實作一個系統差異型的效果,在Android 5.0及之后的系統中,使用背景圖和狀態欄融合的模式,在之前的系統中使用普通的模式,
想要讓背景圖能夠和系統狀態欄融合,需要借助android:fitsSystemWindows這個屬性來實作,
在CoordinatorLayout、AppBarLayout、CollapsingToolbarLayout這種嵌套結構的布局中,將控制元件的android:fitsSystemWindows屬性指定成true,就表示該控制元件會出現在系統狀態欄里,
對應到我們的程式,那就是水果標題欄中的ImageView應該設定這個屬性了,不過只給ImageView設定這個屬性是沒有用的,我們必須將ImageView布局結構中的所有父布局都設定上這個屬性才可以,修改activity_fruit.xml中的代碼,如下所示:
<?xml version="1.0" encoding="utf-8"?>
<androidx.coordinatorlayout.widget.CoordinatorLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_
android:layout_height="match_parent"
android:fitsSystemWindows="true">
<com.google.android.material.appbar.AppBarLayout
android:layout_
android:layout_height="250dp"
android:id="@+id/appBar"
android:fitsSystemWindows="true">
<com.google.android.material.appbar.CollapsingToolbarLayout
android:layout_
android:layout_height="match_parent"
android:id="@+id/collapsing_toolbar"
android:theme="@style/ThemeOverlay.AppCompat.Dark.ActionBar"
android:fitsSystemWindows="true"
app:contentScrim="?attr/colorPrimary"
app:layout_scrollFlags="scroll|exitUntilCollapsed">
<ImageView
android:layout_
android:layout_height="match_parent"
android:id="@+id/fruit_image_view"
android:scaleType="centerCrop"
android:fitsSystemWindows="true"
app:layout_collapseMode="parallax"/>
...
</com.google.android.material.appbar.CollapsingToolbarLayout>
</com.google.android.material.appbar.AppBarLayout>
...
</androidx.coordinatorlayout.widget.CoordinatorLayout>
但是,即使我們將android:fitsSystemWindows屬性都設定好了還是沒有用的,因為還必須在程式的主題中將狀態欄顏色指定成透明色才行,
指定成透明色的方法很簡單,在主題中將android:statusBarColor屬性的值指定成@android:color/transparent就可以了,但問題在于,android:statusBarColor這個屬性是從API 21,也就是Android 5.0系統開始才有的,之前的系統無法指定這個屬性,
那么,系統差異型的功能實作就要從這里開始了,右擊res目錄→New→Directory,創建一個values-v21目錄,然后右擊values-v21目錄→New→Values resource file,創建一個styles.xml檔案,接著對這個檔案進行撰寫,代碼如下所示:
<?xml version="1.0" encoding="utf-8"?>
<resources>
<style name="FruitActivityTheme" parent="Theme.MaterialTest">
<item name="android:statusBarColor">@android:color/transparent</item>
</style>
</resources>
這里定義了一個FruitActivityTheme主題,它是專門給FruitActivity使用的,FruitActivityTheme的parent主題是Theme.MaterialTest,也就是說,它繼承了Theme.MaterialTest中的所有特性,然后我們在FruitActivityTheme中將狀態欄的顏色指定成透明色,由于values-v21目錄是只有Android 5.0及以上的系統才會去讀取的,因此這么宣告是沒有問題的,
但是Android 5.0之前的系統卻無法識別FruitActivityTheme這個主題,因此我們還需要對values/themes.xml檔案進行修改,如下所示:
<resources xmlns:tools="http://schemas.android.com/tools">
<!-- Base application theme. -->
<style name="Theme.MaterialTest" parent="Theme.AppCompat.Light.NoActionBar">
...
</style>
<style name="FruitActivityTheme" parent="Theme.MaterialTest"/>
</resources>
可以看到,這里也定義了一個FruitActivityTheme主題,并且parent主題也是Theme.MaterialTest,但是它的內部是空的,因為Android 5.0之前的系統無法指定狀態欄的顏色,因此這里什么都不用做就可以了,最后,我們還需要讓FruitActivity使用這個主題才可以,修改AndroidManifest.xml中的代碼,如下所示:
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.zhouzhou.materialtest">
<application
android:allowBackup="true"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:roundIcon="@mipmap/ic_launcher_round"
android:supportsRtl="true"
android:theme="@style/Theme.MaterialTest">
<activity
android:name=".FruitActivity"
android:exported="false"
android:theme="@style/FruitActivityTheme"/>
...
</application>
</manifest>
這里使用android:theme屬性單獨給FruitActivity指定了FruitActivityTheme這個主題,這樣我們就大功告成了,現在只要是在Android 5.0及以上的系統運行MaterialTest程式,水果詳情展示界面的效果就會如圖:

12.8 小結與點評
本章是以一個開發者的思維給你講解Material Design,側重于如何去實作這些效果,而實際上,Material Design的設計思維和設計理念才是更加重要的東西,當然這部分內容應該是UI設計人員去學習的,如果你也感興趣的話,可以參考一下Material Design的官方文章:https://material.google.com,