《第一行代碼:Android篇》學習筆記(十二)

来源:https://www.cnblogs.com/1693977889zz/archive/2022/05/11/16256369.html
-Advertisement-
Play Games

本文和接下來的幾篇文章為閱讀郭霖先生所著《第一行代碼:Android(篇第2版)》的學習筆記,按照書中的內容順序進行記錄,書中的Demo本人全部都做過了。 每一章節本人都做了詳細的記錄,以下是我學習記錄(包含大量書中內容的整理和自己在學習中遇到的各種bug及解決方案),方便以後閱讀和查閱。最後,非常 ...


本文和接下來的幾篇文章為閱讀郭霖先生所著《第一行代碼:Android(篇第2版)》的學習筆記,按照書中的內容順序進行記錄,書中的Demo本人全部都做過了。
每一章節本人都做了詳細的記錄,以下是我學習記錄(包含大量書中內容的整理和自己在學習中遇到的各種bug及解決方案),方便以後閱讀和查閱。最後,非常感激郭霖先生提供這麼好的書籍。

第12章 最佳的UI體驗——Material Design實戰

其實長久以來,大多數人都認為Android系統的UI並不算美觀,至少沒有iOS系統的美觀。以至於很多IT公司在進行應用界面設計的時候,為了保證雙平臺的統一性,強制要求Android端的界面風格必須和iOS端一致。

這種情況在現實工作當中實在是太常見了,因為對於一般用戶來說,他們不太可能會在兩個操作系統上分別去使用同一個應用,但是卻必定會在同一個操作系統上使用不同的應用。因此,同一個操作系統中各個應用之間的界面統一性要遠比一個應用在雙平臺的界面統一性重要得多,只有這樣,才能給使用者帶來更好的用戶體驗。

但問題在於,Android標準的界面設計風格並不是特別被大眾所接受,很多公司都覺得自己完全可以設計出更加好看的界面,從而導致Android平臺的界面風格長期難以得到統一。為瞭解決這個問題,谷歌也是祭出了殺手鐧,在2014年Google I/O大會上重磅推出了一套全新的界面設計語言——MaterialDesign

本章我們就將對Material Design進行一次深入的學習。

12.1 什麼是Material Design

Material Design是由谷歌的設計工程師們基於傳統優秀的設計原則,結合豐富的創意和科學技術所發明的一套全新的界面設計語言,包含了視覺、運動、互動效果等特性。那麼谷歌憑什麼認為Material Design就能解決Android平臺界面風格不統一的問題呢?一言以蔽之,好看!

這次谷歌在界面設計上確實是下足了功夫,很多媒體評論,MaterialDesign的出現使得Android首次在UI方面超越了iOS。按照正常的思維來想,如果各個公司都無法設計出比Material Design更出色的界面風格,那麼它們就應該理所當然地使用Material Design來設計界面,從而也就能解決Android平臺界面風格不統一的問題了。

為了做出表率,谷歌從Android 5.0系統開始,就將所有內置的應用都使用Material Design風格來進行設計。可以先欣賞一下,如圖:

image

其中,左邊的應用是Play Store,右邊的應用是YouTube。可以看出,它們的界面都十分美觀,而它們正是使用Material Design來進行設計的。

不過,在重磅推出之後,Material Design的普及程度卻不能說是特別理想。因為這隻是一個推薦的設計規範,主要是面向UI設計人員的,而不是面向開發者的。很多開發者可能根本就搞不清楚什麼樣的界面和效果才叫MaterialDesign,就算搞清楚了,實現起來也會很費勁,因為不少Material Design的效果是很難實現的,而Android中卻幾乎沒有提供相應的API支持,一切都要靠開發者自己從零寫起。

谷歌當然也意識到了這個問題,於是在2015年的Google I/O大會上推出了一個Design Support庫,這個庫將Material Design中最具代表性的一些控制項和效果進行了封裝,使得開發者在即使不瞭解Material Design的情況下也能非常輕鬆地將自己的應用Material化。

本章中我們就將對Design Support這個庫進行深入的學習,並且配合一些其他的控制項來完成一個優秀的MaterialDesign應用。新建一個MaterialTest項目,然後我們馬上開始吧!

12.2 Toolbar

回憶一下,我們曾經在3.4.1小節為了使用一個自定義的標題欄,而把系統原生的ActionBar隱藏掉。沒錯,每個活動最頂部的那個標題欄其實就是ActionBar,之前我們編寫的所有程式里一直都有ActionBar的身影。

不過,ActionBar由於其設計的原因,被限定只能位於活動的頂部,從而不能實現一些Material Design的效果,因此官方現在已經不再建議使用ActionBar了。那麼本書中我也就不准備再介紹ActionBar的用法了,而是直接講解現在更加推薦使用的Toolbar。

Toolbar的強大之處在於,它不僅繼承了ActionBar的所有功能,而且靈活性很高,可以配合其他控制項來完成一些Material Design的效果,下麵我們就來具體學習一下。

首先你要知道,任何一個新建的項目,預設都是會顯示ActionBar的。那麼這個ActionBar到底是從哪裡來的呢?其實這是根據項目中指定的主題來顯示的,打開AndroidManifest.xml文件看一下,如下所示:

<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">
    ...
</application>

可以看到,這裡使用android:theme屬性指定了一個AppTheme的主題。那麼這個AppTheme又是在哪裡定義的呢?打開res/values/themes.xml文件,代碼如下所示:

<resources xmlns:tools="http://schemas.android.com/tools">
    <!-- Base application theme. -->
    <style name="Theme.MaterialTest" parent="Theme.MaterialComponents.DayNight.DarkActionBar">
        <!-- Primary brand color. -->
        <item name="colorPrimary">@color/purple_500</item>
        <item name="colorPrimaryVariant">@color/purple_700</item>
        <item name="colorOnPrimary">@color/white</item>
        <!-- Secondary brand color. -->
        <item name="colorSecondary">@color/teal_200</item>
        <item name="colorSecondaryVariant">@color/teal_700</item>
        <item name="colorOnSecondary">@color/black</item>
        <!-- Status bar color. -->
        <item name="android:statusBarColor" tools:targetApi="l">?attr/colorPrimaryVariant</item>
        <!-- Customize your theme here. -->
    </style>
</resources>

這裡定義了一個叫AppTheme的主題,然後指定它的parent主題是Theme.AppCompat.Light. DarkActionBar。這個DarkActionBar是一個深色的ActionBar主題,我們之前所有的項目中自帶的ActionBar就是因為指定了這個主題才出現的。

現在準備使用Toolbar來替代ActionBar,因此需要指定一個不帶ActionBar的主題,通常有兩種主題可選:

Theme.AppCompat.NoActionBar表示深色主題,它會將界面的主體顏色設成深色,陪襯顏色設成淡色。

Theme.AppCompat.Light.NoActionBar表示淡色主題,它會將界面的主體顏色設成淡色,陪襯顏色設成深色。

具體的效果你可以自己動手試一試,這裡由於我們之前的程式一直都是以淡色為主的,那麼我就選用淡色主題了,如下所示:

<style name="Theme.MaterialTest" parent="Theme.AppCompat.Light.NoActionBar">
<item name="colorPrimaryDark">@color/design_default_color_primary_dark</item>

image

除了上述3個屬性之外,我們還可以通過,android:textColorPrimary,android:windowBackground,和android:navigationBarColor等屬性來控制更多位置的顏色。

colorAccent這個屬性比較難理解,它不只是用來指定這樣一個按鈕的顏色,而是更多表達了一個強調的意思,比如一些控制項的選中狀態也會使用colorAccent的顏色。

現在,我們已經將ActionBar隱藏起來了,那麼接下來看一看如何使用Toolbar來替代ActionBar。修改activity_main.xml中的代碼,如下所示:

<?xml version="1.0" encoding="utf-8"?>
<FrameLayout 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">
    <androidx.appcompat.widget.Toolbar
        android:layout_width="match_parent"
        android:layout_height="?attr/actionBarSize"
        android:id="@+id/toolbar"
        android:background="?colorPrimary"
        android:theme="@style/ThemeOverlay.AppCompat.Dark.ActionBar"
        android:popupTheme="@style/ThemeOverlay.AppCompat.Light"/>
</FrameLayout>

使用xmlns:app指定了一個新的命名空間。正是由於每個佈局文件都會使用xmlns:android來指定一個命名空間,因此我們才能一直使用android:id、android:layout_width等寫法,那麼這裡指定了xmlns:app,也就是說現在可以使用app:attribute這樣的寫法了。

但是為什麼這裡要指定一個xmlns:app的命名空間呢?這是由於Material Design是在Android 5.0系統中才出現的,而很多的Material屬性在5.0之前的系統中並不存在,那麼為了能夠相容之前的老系統,我們就不能使用android:attribute這樣的寫法了,而是應該使用app:attribute。

接下來定義了一個Toolbar控制項,給Toolbar指定了一個id,將它的寬度設置為match_parent,高度設置為actionBar的高度,背景色設置為colorPrimary。

不過下麵的部分就稍微有點難理解了,由於我們剛纔在themes.xml中將程式的主題指定成了淡色主題,因此Toolbar現在也是淡色主題,而Toolbar上面的各種元素就會自動使用深色系,這是為了和主體顏色區別開。但是這個效果看起來就會很差,之前使用ActionBar時文字都是白色的,現在變成黑色的會很難看。

那麼為了能讓Toolbar單獨使用深色主題,這裡我們使用android:theme屬性,將Toolbar的主題指定成了ThemeOverlay.AppCompat.Dark.ActionBar。但是這樣指定完了之後又會出現新的問題,如果Toolbar中有菜單按鈕(我們在2.2.5小節中學過),那麼彈出的菜單項也會變成深色主題,這樣就再次變得十分難看,於是這裡使用了app:popupTheme屬性單獨將彈出的菜單項指定成了淡色主題。之所以使用app:popupTheme,是因為popupTheme這個屬性是在Android 5.0系統中新增的,我們使用app:popupTheme的話就可以相容Android 5.0以下的系統了。

如果你覺得上面的描述很繞的話,可以自己動手做一做試驗,看看不指定上述主題會是什麼樣的效果,這樣你會理解得更加深刻。寫完了佈局,接下來我們修改MainActivity,代碼如下所示:

package com.zhouzhou.materialtest;

import androidx.appcompat.app.AppCompatActivity;
import androidx.appcompat.widget.Toolbar;

import android.os.Bundle;

public class MainActivity extends AppCompatActivity {

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        Toolbar toolbar = (Toolbar) findViewById(R.id.toolbar);
        setSupportActionBar(toolbar);
    }
}

這樣我們就做到既使用了Toolbar,又讓它的外觀與功能都和ActionBar一致了。

image

這個標題欄我們再熟悉不過了,雖然看上去和之前的標題欄沒什麼兩樣,但其實它已經是Toolbar而不是ActionBar了。因此它現在也具備了實現MaterialDesign效果的能力,這個我們在後面就會學到。

接下來我們再學習一些Toolbar比較常用的功能吧,比如修改標題欄上顯示的文字內容。這段文字內容是在AndroidManifest.xml中指定的,如下所示:

...
<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=".MainActivity"
            android:label="Fruits"
            ...
    </application>
    ...

給activity增加了一個android:label屬性,用於指定在Toolbar中顯示的文字內容,如果沒有指定的話,會預設使用application中指定的label內容,也就是我們的應用名稱。

只有一個標題的Toolbar看起來太單調了,還可以再添加一些action按鈕來讓Toolbar更加豐富一些,這裡我提前準備了幾張圖片來作為按鈕的圖標,將它們放在了drawable-xxhdpi目錄下。現在右擊res目錄→New→Directory,創建一個menu文件夾。然後右擊menu文件夾→New→Menu resource file,創建一個toolbar.xml文件,並編寫如下代碼:

<?xml version="1.0" encoding="utf-8"?>
<menu xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto">
    <item
        android:id="@+id/backup"
        android:icon="@drawable/ic_backup"
        android:title="Backup"
        app:showAsAction="always"/>
    <item
        android:id="@+id/delete"
        android:icon="@drawable/ic_delete"
        android:title="Delete"
        app:showAsAction="ifRoom"/>
    <item
        android:id="@+id/settings"
        android:icon="@drawable/ic_settings"
        android:title="Settings"
        app:showAsAction="never"/>
</menu>

可以看到,我們通過<item>標簽來定義action按鈕,android:id用於指定按鈕的id, android:icon用於指定按鈕的圖標,android:title用於指定按鈕的文字。

接著使用app:showAsAction來指定按鈕的顯示位置,之所以這裡再次使用了app命名空間,同樣是為了能夠相容低版本的系統。showAsAction主要有以下幾種值可選:

  • always表示永遠顯示在Toolbar中,如果屏幕空間不夠則不顯示;
  • ifRoom表示屏幕空間足夠的情況下顯示在Toolbar中,不夠的話就顯示在菜單當中;
  • never則表示永遠顯示在菜單當中。

註意,Toolbar中的action按鈕只會顯示圖標,菜單中的action按鈕只會顯示文字。接下來的做法就和2.2.5小節中的完全一致了,修改MainActivity中的代碼,如下所示:

package com.zhouzhou.materialtest;

import androidx.annotation.NonNull;
import androidx.appcompat.app.AppCompatActivity;
import androidx.appcompat.widget.Toolbar;

import android.os.Bundle;
import android.view.Menu;
import android.view.MenuItem;
import android.widget.Toast;

public class MainActivity extends AppCompatActivity {

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        Toolbar toolbar = (Toolbar) findViewById(R.id.toolbar);
        setSupportActionBar(toolbar);
    }
    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;
            default:
        }
        return true;
    }
}

在onCreateOptionsMenu()方法中載入了toolbar.xml這個菜單文件,然後在onOptionsItemSelected()方法中處理各個按鈕的點擊事件。現在重新運行一下程式,效果如圖:

image

可以看到,Toolbar上面現在顯示了兩個action按鈕,這是因為Backup按鈕指定的顯示位置是always, Delete按鈕指定的顯示位置是ifRoom,而現在屏幕空間很充足,因此兩個按鈕都會顯示在Toolbar中。

另外一個Settings按鈕由於指定的顯示位置是never,所以不會顯示在Toolbar中,點擊一下最右邊的菜單按鈕來展開菜單項,就能找到Settings按鈕了(菜單中的action按鈕只會顯示文字)。另外,這些action按鈕都是可以響應點擊事件的,你可以自己去試一試。Toolbar的功能還遠遠不只這些,後面會結合其他控制項來挖掘Toolbar的更多功能。

12.3 滑動菜單

滑動菜單可以說是Material Design中最常見的效果之一了,在許多著名的應用(如Gmail、Google+等)中,都有滑動菜單的功能。雖說這個功能看上去好像挺複雜的,不過藉助谷歌提供的各種工具,我們可以很輕鬆地實現非常炫酷的滑動菜單效果。

12.3.1 DrawerLayout

所謂的滑動菜單就是將一些菜單選項隱藏起來,而不是放置在主屏幕上,然後可以通過滑動的方式將菜單顯示出來。這種方式既節省了屏幕空間,又實現了非常好的動畫效果,是Material Design中推薦的做法。

不過,如果我們全靠自己去實現上述功能的話,難度恐怕就很大了。幸運的是,谷歌提供了一個DrawerLayout控制項,藉助這個控制項,實現滑動菜單簡單又方便。

DrawerLayout的用法,首先它是一個佈局,在佈局中允許放入兩個直接子控制項,第一個子控制項是主屏幕中顯示的內容,第二個子控制項是滑動菜單中顯示的內容。因此,我們就可以對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_width="match_parent"
    android:layout_height="match_parent">
    <FrameLayout
        android:layout_width="match_parent"
        android:layout_height="match_parent">
        <androidx.appcompat.widget.Toolbar
            android:id="@+id/toolbar"
            android:layout_width="match_parent"
            android:layout_height="?attr/actionBarSize"
            android:background="?attr/colorPrimary"
            android:theme="@style/ThemeOverlay.AppCompat.Dark.ActionBar"
            android:popupTheme="@style/ThemeOverlay.AppCompat.Light"/>
    </FrameLayout>
    <TextView
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:gravity="start"
        android:text="This is menu"
        android:textSize="30sp"
        android:background="#FFF"/>
</androidx.drawerlayout.widget.DrawerLayout>

DrawerLayout中放置了兩個直接子控制項:

  • 第一個子控制項是FrameLayout,用於作為主屏幕中顯示的內容,當然裡面還有我們剛剛定義的Toolbar。
  • 第二個子控制項這裡使用了一個TextView,用於作為滑動菜單中顯示的內容,其實使用什麼都可以,DrawerLayout並沒有限制只能使用固定的控制項。

第二個子控制項有一點需要註意,android:gravity="start"這個屬性是必須指定的,因為我們需要告訴DrawerLayout滑動菜單是在屏幕的左邊還是右邊:

  • 指定left表示滑動菜單在左邊;

  • 指定right表示滑動菜單在右邊;

  • 指定start,表示會根據系統語言進行判斷;

    如果系統語言是從左往右的,比如英語、漢語,滑動菜單就在左邊,如果系統語言是從右往左的,比如阿拉伯語,滑動菜單就在右邊。

只需要改動這麼多就可以了,現在重新運行一下程式,然後在屏幕的左側邊緣向右拖動,就可以讓滑動菜單顯示出來了,如圖:

image

然後向左滑動菜單,或者點擊一下菜單以外的區域,都可以讓滑動菜單關閉,從而回到主界面。無論是展示還是隱藏滑動菜單,都是有非常流暢的動畫過渡的。

可以看到,我們只是稍微改動了一下佈局文件,就能實現如此炫酷的效果。

過現在的滑動菜單還有點問題,因為只有在屏幕的左側邊緣進行拖動時才能將菜單拖出來,而很多用戶可能根本就不知道有這個功能,那麼該怎麼提示他們呢?

Material Design建議的做法是在Toolbar的最左邊加入一個導航按鈕,點擊了按鈕也會將滑動菜單的內容展示出來。這樣就相當於給用戶提供了兩種打開滑動菜單的方式,防止一些用戶不知道屏幕的左側邊緣是可以拖動的。

下麵,我們開始來實現這個功能。首先我準備了一張導航按鈕的圖標ic_menu.png,將它放在了drawable-xxhdpi目錄下。然後修改MainActivity中的代碼,如下所示:

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);
        ActionBar actionBar = getSupportActionBar();
        if (actionBar != null) {
            actionBar.setDisplayHomeAsUpEnabled(true);
            actionBar.setHomeAsUpIndicator(R.drawable.ic_menu);
        }
    }
    ...
    @Override
    public boolean onOptionsItemSelected(@NonNull MenuItem item) {
        switch (item.getItemId()) {
            case R.id.backup:
                ...
            case android.R.id.home:
                mDrawerLayout.openDrawer(GravityCompat.START);
                break;
            default:
        }
        return true;
    }
}

首先調用findViewById()方法得到了DrawerLayout的實例,然後調用getSupportActionBar()方法得到了ActionBar的實例,雖然這個ActionBar的具體實現是由Toolbar來完成的。接著調用ActionBar的setDisplayHomeAsUpEnabled()方法讓導航按鈕顯示出來,又調用了setHomeAsUpIndicator()方法來設置一個導航按鈕圖標。

實際上,Toolbar最左側的這個按鈕就叫作HomeAsUp按鈕,它預設的圖標是一個返回的箭頭,含義是返回上一個活動。很明顯,這裡我們將它預設的樣式和作用都進行了修改。

接下來在onOptionsItemSelected()方法中對HomeAsUp按鈕的點擊事件進行處理,HomeAsUp按鈕的id永遠都是android.R.id.home。然後調用DrawerLayout的openDrawer()方法將滑動菜單展示出來,註意openDrawer()方法要求傳入一個Gravity參數,為了保證這裡的行為和XML中定義的一致,我們傳入了GravityCompat.START。

現在重新運行一下程式,效果如圖:

image

可以看到,在Toolbar的最左邊出現了一個導航按鈕,用戶看到這個按鈕就知道這肯定是可以點擊的。現在點擊一下這個按鈕,滑動菜單界面就會再次展示出來了。

12.3.2 NavigationView

目前我們已經成功實現了滑動菜單功能,其中滑動功能已經做得非常好了,但是菜單卻還很醜,畢竟菜單頁面僅僅使用了一個TextView,非常單調。有對比才會有落差,我們看一下Google+的滑動菜單頁面是長什麼樣的,如圖:

image

事實上,你可以在滑動菜單頁面定製任意的佈局,不過谷歌給我們提供了一種更好的方法——使用NavigationView。NavigationView是Design Support庫中提供的一個控制項,它不僅是嚴格按照Material Design的要求來進行設計的,而且還可以將滑動菜單頁面的實現變得非常簡單。接下來我們就學習一下NavigationView的用法。

首先,既然這個控制項是Design Support庫中提供的,那麼我們就需要將這個庫引入到項目中才行。打開app/build.gradle文件,在dependencies閉包中添加如下內容:

dependencies {
    implementation 'com.android.support:design:28.0.0'
    implementation 'de.hdodenhof:circleimageview:2.1.0'
}

註意:此時implementation 'com.android.support:design:28.0.0'會有爆紅!

image

解決方式:推薦使用androidx下的庫。

第一種解決方式:

image

第二種解決方式:

將最新的com.android.support:design:28.0.0改為com.google.android.material:material:1.0.0

dependencies {
    implementation 'com.google.android.material:material:1.0.0'
    implementation 'de.hdodenhof:circleimageview:2.1.0'
    implementation 'androidx.appcompat:appcompat:1.3.0'
    implementation 'com.google.android.material:material:1.4.0'
    implementation 'androidx.constraintlayout:constraintlayout:2.0.4'
    testImplementation 'junit:junit:4.13.2'
    androidTestImplementation 'androidx.test.ext:junit:1.1.3'
    androidTestImplementation 'androidx.test.espresso:espresso-core:3.4.0'
}

這裡添加了兩行依賴關係,第一行就是Design Support庫,第二行是一個開源項目CircleImageView,它可以用來輕鬆實現圖片圓形化的功能,我們待會就會用到它。CircleImageView的項目主頁地址是:https://github.com/hdodenhof/CircleImageView。

在開始使用NavigationView之前,我們還需要提前準備好兩個東西:menu和headerLayout。menu是用來在NavigationView中顯示具體的菜單項的,headerLayout則是用來在NavigationView中顯示頭部佈局的。

我們先來準備menu,這裡我事先找了幾張圖片來作為按鈕的圖標,並將它們放在了drawable-xxhdpi目錄下。然後右擊menu文件夾→New→Menuresource file,創建一個nav_menu.xml文件,並編寫如下代碼:

<?xml version="1.0" encoding="utf-8"?>
<menu xmlns:android="http://schemas.android.com/apk/res/android">
    <group android:checkableBehavior="single">
        <item
            android:id="@+id/nav_call"
            android:icon="@drawable/nav_call"
            android:title="Call" />
        <item
            android:id="@+id/nav_friends"
            android:icon="@drawable/nav_friends"
            android:title="Friends"/>
        <item
            android:id="@+id/nav_location"
            android:icon="@drawable/nav_location"
            android:title="Location"/>
        <item
            android:id="@+id/nav_mail"
            android:icon="@drawable/nav_mail"
            android:title="Mail"/>
        <item
            android:id="@+id/nav_task"
            android:icon="@drawable/nav_tasks"
            android:title="Tasks"/>
    </group>
</menu>

首先在

中嵌套了一個標簽,然後將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_width="match_parent"
    android:layout_height="180dp"
    android:padding="10dp"
    android:background="?attr/colorPrimary">
    <de.hdodenhof.circleimageview.CircleImageView
        android:layout_width="70dp"
        android:layout_height="70dp"
        android:id="@+id/icon_image"
        android:src="@drawable/nav_icon"
        android:layout_centerInParent="true"/>
    <TextView
        android:layout_width="wrap_content"
        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_width="wrap_content"
        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_width="match_parent"
    android:layout_height="match_parent">
    <FrameLayout
        android:layout_width="match_parent"
        android:layout_height="match_parent">
        <androidx.appcompat.widget.Toolbar
            android:id="@+id/toolbar"
            android:layout_width="match_parent"
            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_width="match_parent"
        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左側的導航按鈕,效果如圖:

image

MaterialDesign的魅力就在這裡,它真的是一種非常美觀的設計理念,只要你按照它的各種規範和建議來設計界面,最終做出來的程式就是特別好看的。

12.4 懸浮按鈕和可交互提示

立面設計是Material Design中一條非常重要的設計思想,也就是說,按照Material Design的理念,應用程式的界面不僅僅只是一個平面,而應該是有立體效果的。在官方給出的示例中,最簡單且最具代表性的立面設計就是懸浮按鈕了,這種按鈕不屬於主界面平面的一部分,而是位於另外一個維度的,因此就會給人一種懸浮的感覺。

本節中我們會對這個懸浮按鈕的效果進行學習,另外還會學習一種可互動式的提示工具。關於提示工具,我們之前一直都是使用的Toast,但是Toast只能用於告知用戶某某事情已經發生了,用戶卻不能對此做出任何的響應,那麼今天我們就將在這一方面進行擴展。

12.4.1 FloatingActionButton

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_width="match_parent"
    android:layout_height="match_parent">
    <FrameLayout
        android:layout_width="match_parent"
        android:layout_height="match_parent">
        <androidx.appcompat.widget.Toolbar
            android:id="@+id/toolbar"
            android:layout_width="match_parent"
            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_width="wrap_content"
        android:layout_height="wrap_content"
        android:id="@+id/fab"
        android:layout_gravity="bottom|end"
        android:layout_margin="16dp"
        android:src="@drawable/ic_done"/>
    <com.google.android.material.navigation.NavigationView
        android:layout_width="match_parent"
        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設置了一個圖標。

就是這麼簡單,現在我們就可以來運行一下了,效果如圖:

image

一個漂亮的懸浮按鈕就在屏幕的右下方出現了。

仔細觀察的話,會發現這個懸浮按鈕的下麵還有一點陰影,因為FloatingActionButton是懸浮在當前界面上的,既然是懸浮,那麼就理所應當會有投影,Design Support庫連這種細節都幫我們考慮到了。說到懸浮,其實我們還可以指定FloatingActionButton的懸浮高度,如下所示:

<com.google.android.material.floatingactionbutton.FloatingActionButton
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:id="@+id/fab"
        android:layout_gravity="bottom|end"
        android:layout_margin="16dp"
        android:src="@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,效果如圖:

image

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顯示出來。現在重新運行一下程式,並點擊懸浮按鈕,效果如圖:

image

可以看到,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_width="match_parent"
    android:layout_height="match_parent">
    <androidx.coordinatorlayout.widget.CoordinatorLayout
        android:layout_width="match_parent"
        android:layout_height="match_parent">
        <androidx.appcompat.widget.Toolbar
            android:id="@+id/toolbar"
            android:layout_width="match_parent"
            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_width="wrap_content"
        android:layout_height="wrap_content"
        android:id="@+id/fab"
        android:layout_gravity="bottom|end"
        android:layout_margin="16dp"
        android:src="@drawable/ic_done"
        app:elevation="8dp"/>
    </androidx.coordinatorlayout.widget.CoordinatorLayout>
    <com.google.android.material.navigation.NavigationView
        android:layout_width="match_parent"
        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,因此這種替換不會有任何的副作用。現在重新運行一下程式,並點擊懸浮按鈕,效果如圖:

image

可以看到,懸浮按鈕自動向上偏移了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_width="match_parent"
        android:layout_height="wrap_content"
        app:cardCornerRadius="4dp"
        app:cardElevation="5dp">
        <TextView
            android:layout_width="match_parent"
            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_width="match_parent"
    android:layout_height="match_parent">
    <androidx.coordinatorlayout.widget.CoordinatorLayout
        android:layout_width="match_parent"
        android:layout_height="match_parent">
        <androidx.appcompat.widget.Toolbar
            android:id="@+id/toolbar"
            android:layout_width="match_parent"
            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_width="match_parent"
            android:layout_height="match_parent"
            android:id="@+id/recycler_view"/>

    <com.google.android.material.floatingactionbutton.FloatingActionButton
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:id="@+id/fab"
        android:layout_gravity="bottom|end"
        android:layout_margin="16dp"
        android:src="@drawable/ic_done"
        app:elevation="8dp"/>
    </androidx.coordinatorlayout.widget.CoordinatorLayout>
    <com.google.android.material.navigation.NavigationView
        android:layout_width="match_parent"
        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_width="match_parent"
    android:layout_height="wrap_content"
    android:layout_margin="5dp"
    app:cardCornerRadius="4dp">
    <LinearLayout
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:orientation="vertical">
        <ImageView
            android:layout_width="match_parent"
            android:layout_height="100dp"
            android:id="@+id/fruit_image"
            android:scaleType="centerCrop"/>
        <TextView
            android:layout_width="wrap_content"
            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 = (F

您的分享是我們最大的動力!

-Advertisement-
Play Games
更多相關文章
  • 這邊文章聊聊自己對數據治理開發實踐的一些思路,就是聊聊怎麼開始去做數據治理這件事情。說起數據治理,有時候雖然看了很多文章,看了很多的介紹,瞭解數據治理的理論,但是實際上需要我們去搞的時候,就會踩很多的坑。這裡記一下自己做數據治理的一些思路,做做筆記,也分享給需要的同學。 當然,想要做數據治理,想要學 ...
  • 在數倉項目中,我們常常會選擇Apache Atlas進行數據的治理。本文結合筆者在生產環境中遇到的常見問題及解決方法,整合出完整的Atlas編譯、部署及使用過程。 ...
  • 1、高可用性的目的是什麼? 高可用性的目標是以最小的停機時間提供連續的服務(唯一真正具有 "零 "停機時間的設備是心臟起搏器和核武器中的安全裝置)。這意味著,如果一個組件發生故障,另一個組件可以立即接管其功能,而不會實質性地中斷對系統用戶的服務。高可用性還要求有能力檢測到一個或多個組件發生故障,然後 ...
  • 上一篇文章我們演示瞭如何《在 S3 備份恢復 RadonDB MySQL 集群數據》,本文將演示在 KubeSphere[1] 中使用 Prometheus[2] + Grafana[3] 構建 MySQL 監控平臺,開啟所需監控指標。 背景 Prometheus 基於文本的暴露格式,已經成為雲原生 ...
  • 聲明:全文來源《mysql SQL必知必會(第3版)》 第一章 瞭解SQL 1.1 資料庫基礎 資料庫(database)保存有組織的數據的容器 表(table)某種特定類型數據的結構化清單。資料庫中的每個表都有一個用來標識自己的名字。此名字是唯一的。 模式(schema)關於資料庫和表的佈局及特性 ...
  • 本文介紹什麼是通配符、如何使用通配符,以及怎樣使用 SQL LIKE 操作符進行通配搜索,以便對數據進行複雜過濾。 一、LIKE 操作符 前面介紹的所有操作符都是針對已知值進行過濾的。不管是匹配一個值還是多個值,檢驗大於還是小於已知值,或者檢查某個範圍的值,其共同點是過濾中使用的值都是已知的。 但是 ...
  • 本文和接下來的幾篇文章為閱讀郭霖先生所著《第一行代碼:Android(篇第2版)》的學習筆記,按照書中的內容順序進行記錄,書中的Demo本人全部都做過了。 每一章節本人都做了詳細的記錄,以下是我學習記錄(包含大量書中內容的整理和自己在學習中遇到的各種bug及解決方案),方便以後閱讀和查閱。最後,非常 ...
  • 本文和接下來的幾篇文章為閱讀郭霖先生所著《第一行代碼:Android(篇第2版)》的學習筆記,按照書中的內容順序進行記錄,書中的Demo本人全部都做過了。 每一章節本人都做了詳細的記錄,以下是我學習記錄(包含大量書中內容的整理和自己在學習中遇到的各種bug及解決方案),方便以後閱讀和查閱。最後,非常 ...
一周排行
    -Advertisement-
    Play Games
  • Dapr Outbox 是1.12中的功能。 本文只介紹Dapr Outbox 執行流程,Dapr Outbox基本用法請閱讀官方文檔 。本文中appID=order-processor,topic=orders 本文前提知識:熟悉Dapr狀態管理、Dapr發佈訂閱和Outbox 模式。 Outbo ...
  • 引言 在前幾章我們深度講解了單元測試和集成測試的基礎知識,這一章我們來講解一下代碼覆蓋率,代碼覆蓋率是單元測試運行的度量值,覆蓋率通常以百分比表示,用於衡量代碼被測試覆蓋的程度,幫助開發人員評估測試用例的質量和代碼的健壯性。常見的覆蓋率包括語句覆蓋率(Line Coverage)、分支覆蓋率(Bra ...
  • 前言 本文介紹瞭如何使用S7.NET庫實現對西門子PLC DB塊數據的讀寫,記錄了使用電腦模擬,模擬PLC,自至完成測試的詳細流程,並重點介紹了在這個過程中的易錯點,供參考。 用到的軟體: 1.Windows環境下鏈路層網路訪問的行業標準工具(WinPcap_4_1_3.exe)下載鏈接:http ...
  • 從依賴倒置原則(Dependency Inversion Principle, DIP)到控制反轉(Inversion of Control, IoC)再到依賴註入(Dependency Injection, DI)的演進過程,我們可以理解為一種逐步抽象和解耦的設計思想。這種思想在C#等面向對象的編 ...
  • 關於Python中的私有屬性和私有方法 Python對於類的成員沒有嚴格的訪問控制限制,這與其他面相對對象語言有區別。關於私有屬性和私有方法,有如下要點: 1、通常我們約定,兩個下劃線開頭的屬性是私有的(private)。其他為公共的(public); 2、類內部可以訪問私有屬性(方法); 3、類外 ...
  • C++ 訪問說明符 訪問說明符是 C++ 中控制類成員(屬性和方法)可訪問性的關鍵字。它們用於封裝類數據並保護其免受意外修改或濫用。 三種訪問說明符: public:允許從類外部的任何地方訪問成員。 private:僅允許在類內部訪問成員。 protected:允許在類內部及其派生類中訪問成員。 示 ...
  • 寫這個隨筆說一下C++的static_cast和dynamic_cast用在子類與父類的指針轉換時的一些事宜。首先,【static_cast,dynamic_cast】【父類指針,子類指針】,兩兩一組,共有4種組合:用 static_cast 父類轉子類、用 static_cast 子類轉父類、使用 ...
  • /******************************************************************************************************** * * * 設計雙向鏈表的介面 * * * * Copyright (c) 2023-2 ...
  • 相信接觸過spring做開發的小伙伴們一定使用過@ComponentScan註解 @ComponentScan("com.wangm.lifecycle") public class AppConfig { } @ComponentScan指定basePackage,將包下的類按照一定規則註冊成Be ...
  • 操作系統 :CentOS 7.6_x64 opensips版本: 2.4.9 python版本:2.7.5 python作為腳本語言,使用起來很方便,查了下opensips的文檔,支持使用python腳本寫邏輯代碼。今天整理下CentOS7環境下opensips2.4.9的python模塊筆記及使用 ...