1. 程式人生 > 實用技巧 >《第一行程式碼》閱讀筆記(三十四)——Material Design

《第一行程式碼》閱讀筆記(三十四)——Material Design

ToolBar

首先:注意使用androidx.appcompat.widget.Toolbar而不是android.widget.Toolbar

在這一章節中,作者主要介紹了幾個知識點。筆者在此梳理一些

  1. xmlns:app

——第一行程式碼
這裡使用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。

瞭解一下就行了,這裡嗎還涉及到一些xml中schemas技術。現在向下相容做的並不多了,而這種名稱空間的方式仍然經常使用於自定義控制元件的自定義屬性宣告,所以還是很有必要知道是怎麼回事的,但是不用深究。

  1. 主題

這裡講了一大堆,筆者沒整明白怎麼回事,實話說。不過不重要

  1. 自定義actionbar

就很簡單,第一步隱藏系統的actionbar,第二步傳入toolbar,方法如下:

Toolbar toolbar = findViewById(R.id.toolbar);
        setSupportActionBar(toolbar);
  1. 修改toolbar文字

android:label="Fruits"


其實label標籤就是AppName,而toolbar上面的文字就是AppName

  1. Menu屬性

——第一行程式碼
可以看到,我們通過標籤來定義action 按鈕,android:id用於指定按鈕的id,android: icon用於指定按鈕的圖示,android:title用於指定按鈕的文字。接著使用app: showAsAction來指定按鈕的顯示位置,之所以這裡再次使用了app名稱空間,同樣是為了能夠相容低版本的系統。showAsAction 主要有以下幾種值可選:always表示永遠顯示在Toolbar中,如果螢幕空間不夠則不顯示;ifRoom表示螢幕空間足夠的情況下顯示在Toolbar中,不夠的話就顯示在選單當中;never 則表示永遠顯示在選單當中。注意,Toolbar中的action按鈕只會顯示圖示,選單中的action 按鈕只會顯示文字。

DrawerLayout

——第一行程式碼
但是關於第二個子控制元件有一點需要注意,layout_ gravity 這個屬性是必須指定的,因為我們需要告訴DrawerLayout滑動選單是在螢幕的左邊還是右邊,指定left 表示滑動選單在左邊,指定right表示滑動選單在右邊。這裡我指定了start, 表示會根據系統語言進行判斷,如果系統語言是從左往右的,比如英語、漢語,滑動選單就在左邊,如果系統語言是從右往左的,比如阿拉伯語,滑動選單就在右邊。

還有一個要注意的就是現在很多全面屏手機,側邊左右滑動是返回的操作,需要避免誤觸才能出發滑動選單。解決方法,一般手機頂部1/5部分左右滑動不是返回,或者關閉側邊操作。

作者介紹了一個小demo還是很有意思的,展示一下
第一步:編寫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="?android:attr/actionBarSize"
            android:background="?android:attr/colorPrimary"
            android:theme="@style/ThemeOverlay.AppCompat.Dark.ActionBar"
            app:popupTheme="@style/Theme.AppCompat.Light" />

    </FrameLayout>

    <TextView
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:background="#22FFAF"
        android:layout_gravity="start"
        android:text="this is menu" />
</androidx.drawerlayout.widget.DrawerLayout>

第二步:編寫Activity

public class MainActivity extends AppCompatActivity {

    private DrawerLayout mDrawerLayout;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        Toolbar toolbar = findViewById(R.id.toolbar);
        setSupportActionBar(toolbar);
        mDrawerLayout = findViewById(R.id.drawer_layout);
        ActionBar actionBar = getSupportActionBar();
        if (actionBar != null) {
            actionBar.setDisplayHomeAsUpEnabled(true);
        }
    }

    @Override
    public boolean onOptionsItemSelected(@NonNull MenuItem item) {
        switch (item.getItemId()) {
            case android.R.id.home:
                mDrawerLayout.openDrawer(GravityCompat.START);
        }
        return true;
    }
}

主要就是兩點,第一點actionBar.setDisplayHomeAsUpEnabled(true);啟用預設HomeAsUp按鈕,其實就是一個小箭頭。setHomeAsUpIndicator()設定圖示。

第二點就是通過onOptionsItemSelected編輯點選事件,android.R.id.home是控制元件的名字。mDrawerLayout.openDrawer(GravityCompat.START);就是啟動滑動選單的方法。注意openDrawer()方法要求傳入一個Gravity引數,為了保證這裡的行為和XML中定義的一致,我們傳入了GravityCompat.START。

NavigationView

implementation 'com.google.android.material:material:1.1.0'
implementation 'de.hdodenhof:circleimageview:3.1.0'
書中引用的兩個依賴需要更換成這兩個新的

在佈局方面都是非常簡單的

<de.hdodenhof.circleimageview.CircleImageView
        android:id="@+id/icon_image"
        android:layout_width="70dp"
        android:layout_height="70dp"
        android:layout_centerInParent="true"
        android:src="@drawable/nav_icon" />

CircleImageView就和imageView一模一樣的使用

<com.google.android.material.navigation.NavigationView
        android:id="@+id/nav_view"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:layout_gravity="start"
        app:menu="@menu/nav_menu"
        app:headerLayout="@layout/nav_header"/>

和普通控制元件沒啥區別,主要是多了很多app的屬性,書中使用到的就是menu和headerLayout,分別傳入一個menu和一個佈局。

 NavigationView navigationView = findViewById(R.id.nav_view);
        //設定預設選項
        navigationView.setCheckedItem(R.id.nav_call);
        //設定選項的點選事件
        navigationView.setNavigationItemSelectedListener(new NavigationView.OnNavigationItemSelectedListener() {
            @Override
            public boolean onNavigationItemSelected(@NonNull MenuItem item) {
                mDrawerLayout.closeDrawers();
                return true;
            }
        });

FloatActionButton

<com.google.android.material.floatingactionbutton.FloatingActionButton
            android:id="@+id/fab"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_gravity="bottom|end"
            android:layout_margin="16dp"
            android:src="@drawable/nav_icon"
            app:elevation="100dp"/>

特殊的地方在於app:elevation="100dp"表示懸浮的高度,但是筆者沒有發現明顯的變化。除此以外與button沒有差別。

——第一行程式碼
這裡使用app:elevation屬性來給FloatingActionButton 指定一個高度值,高度值越大,投影範圍也越大,但是投影效果越淡,高度值越小,投影範圍也越小,但是投影效果越濃。當然這些效果的差異其實都不怎麼明顯,我個人感覺使用預設的FloatingActionButton效果就已經足夠了。

Snackbar

——第一行程式碼
首先要明確,Snackbar並不是Toast的替代品,它們兩者之間有著不同的應用場景。Toast 的作用是告訴使用者現在發生了什麼事情,但同時使用者只能被動接收這個事情,因為沒有什麼辦法能讓使用者進行選擇。而Snackbar則在這方面進行了擴充套件,它允許在提示當中加入一個可互動按鈕,當用戶點選按鈕的時候可以執行一些額外的邏輯操作。打個比方,如果我們在執行刪除操作的時候只彈出一個Toast提示,那麼使用者要是誤刪了某個重要資料的話肯定會十分抓狂吧,但是如果我們增加一個Undo按鈕,就相當於給使用者提供了一種彌補措施,從而大大降低了事故發生的概率,提升了使用者體驗。

FloatingActionButton fab = findViewById(R.id.fab);
        fab.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                Snackbar.make(v, "Data deleted", Snackbar.LENGTH_SHORT)
                        .setAction("Undo", new View.OnClickListener() {
                            @Override
                            public void onClick(View v) {
                                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顯示出來。

CoordinatorLayout

在使用Snackbar的過程中,我們發現彈出的提示會遮擋FloatActionButton。解決這個問題的辦法就是使用CoordinatorLayout。

——第一行程式碼
事實上,CoordinatorLayout可以監聽其所有子控制元件的各種事件,然後自動幫助我們做出最為合理的響應。舉個簡單的例子,剛才彈出的Snackbar 提示將懸浮按鈕遮擋住了,而如果我們能讓CoordinatorLayout監聽到Snackbar的彈出事件,那麼它會自動將內部的FloatingActionButton向上偏移,從而確保不會被Snackbar遮擋到。

使用的方法就是用androidx.coordinatorlayout.widget.CoordinatorLayout替換FrameLayout

CardView

implementation 'androidx.recyclerview:recyclerview:1.1.0'
implementation 'com.google.android.material:material:1.1.0'
implementation 'com.github.bumptech.glide:glide:4.11.0'

書中引用的依賴需要更換成這新的

——第一行程式碼
注意上述宣告的最後一行, 這裡添加了一個Glide庫的依賴。Glide是一個超級強大的圖片載入庫,它不僅可以用於載入本地圖片,還可以載入網路圖片、GIF 圖片、甚至是本地視訊。最重要的是,Glide的用法非常簡單,只需一行程式碼就能輕鬆實現複雜的圖片載入功能,因此這裡我們準備用它來載入水果圖片。Glide的專案主頁地址是: htps:/github.com/bumptech/glide。

CardView只是一個非常簡單的控制元件,可以去掉圓角,之前筆者介紹過的shape也是可以完成的。

recycleView的部分相信大家都能得心應手了,所以這裡主要介紹一下Glide

Glide.with(mContext)
                .load(xxx)
                .placeholder(mContext.getResources().getDrawable(R.drawable.ic_launcher_background))
                .into(view);

首先呼叫Glide.with()方法並傳人一個Context、Activity 或Fragment引數。然後呼叫load()方法去載入圖片,可以是一個 URL地址,也可以是一個本地路徑,或者是一個資源id。再然後就是placeholder()方法是用來顯示載入過程中的過渡圖片的,從網路上面獲取圖片往往較慢,需要使用本地的圖片過渡一下,以免給使用者帶來不好的體驗。最後呼叫into()方法將圖片設定到具體某一個ImageView中就可以了。

AppBarLayout

AppBarLayout就是為了解決之前在CoordinatorLayout中recyclerview和toolbar衝突的問題。
首先使用AppBarLayout包裹toolbar。
然後在recyclerview中新增屬性app:layout_behavior = "@string/appbar_scrolling_view_behavior"
即可解決衝突

優化:
在toolbar中中新增屬性app:layout_scrollFlags="scroll|enterAlways|snap"即可實現再向下滑動過程中隱藏toolbar,當再次返回頂端時,重現。

——第一行程式碼
這裡在Toolbar中添加了一個app:layout_ scrollFlags屬性,並將這個屬性的值指定成了scroll|enterAlways |snap。其中,scroll表示當RecyclerView向上滾動的時候,Toolbar會跟著一起向上滾動並實現隱藏;enterAlways表示當RecyclerView向下滾動的時候,Toolbar會跟著一起向下滾動並重新顯示。snap表示當Toolbar還沒有完全隱藏或顯示的時候,會根據當前滾動的距離,自動選擇是隱藏還是顯示。

下拉重新整理

SwipeRefreshLayout就是用於實現下拉重新整理功能的核心類。使用SwipeRefreshLayout需要匯入下面的依賴
implementation 'androidx.swiperefreshlayout:swiperefreshlayout:1.1.0'

 <androidx.swiperefreshlayout.widget.SwipeRefreshLayout
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        app:layout_behavior="@string/appbar_scrolling_view_behavior">

        <androidx.recyclerview.widget.RecyclerView
            android:layout_width="wrap_content"
            android:layout_height="wrap_content" />

    </androidx.swiperefreshlayout.widget.SwipeRefreshLayout>

Activity

public class MainActivity extends AppCompatActivity {

    private SwipeRefreshLayout swipeRefreshLayout;
    
    ...

        swipeRefreshLayout = findViewById(R.id.refresh);
        swipeRefreshLayout.setColorSchemeResources(R.color.colorPrimary);
        swipeRefreshLayout.setOnRefreshListener(new SwipeRefreshLayout.OnRefreshListener() {
            @Override
            public void onRefresh() {
                refresh();
            }
        });
    }

    private void refresh() {
        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()方法,然後我們在這裡去處理具體的重新整理邏輯就可以了。

CollapsingToolbarLayout

CollapsingToolbarLayout是不能獨立存在的,它在設計的時候就被限定只能作為AppBarLayout的直接子佈局來使用。而AppBarLayout又必須是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_width="match_parent"
    android:layout_height="match_parent">

    <com.google.android.material.appbar.AppBarLayout
        android:id="@+id/appBar"
        android:layout_width="match_parent"
        android:layout_height="250dp">

        <com.google.android.material.appbar.CollapsingToolbarLayout
            android:id="@+id/collapsing_toolbar"
            android:layout_width="match_parent"
            android:layout_height="match_parent"
            android:theme="@style/ThemeOverlay.AppCompat.Dark.ActionBar"
            app:contentScrim="@color/colorPrimary"
            app:layout_scrollFlags="scroll|exitUntilCollapsed">

            <ImageView
                android:id="@+id/fruit_image_view"
                android:layout_width="match_parent"
                android:layout_height="match_parent"
                android:scaleType="centerCrop"
                app:layout_collapseMode="parallax" />

            <androidx.appcompat.widget.Toolbar
                android:id="@+id/toolbar"
                android:layout_width="match_parent"
                android:layout_height="?attr/actionBarSize"
                app:layout_collapseMode="pin" />
        </com.google.android.material.appbar.CollapsingToolbarLayout>

    </com.google.android.material.appbar.AppBarLayout>

    <androidx.core.widget.NestedScrollView
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        app:layout_behavior="@string/appbar_scrolling_view_behavior">

        <LinearLayout
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:orientation="vertical">

            <androidx.cardview.widget.CardView
                android:layout_width="match_parent"
                android:layout_height="wrap_content"
                android:layout_margin="15dp"
                android:layout_marginTop="35dp"
                app:cardCornerRadius="4dp">

                <TextView
                    android:id="@+id/fruit_content_text"
                    android:layout_width="wrap_content"
                    android:layout_height="wrap_content"
                    android:layout_margin="10dp" />
            </androidx.cardview.widget.CardView>

        </LinearLayout>
    </androidx.core.widget.NestedScrollView>

    <com.google.android.material.floatingactionbutton.FloatingActionButton
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_margin="16dp"
        android:src="@drawable/nav_icon"
        app:layout_anchor="@id/appBar"
        app:layout_anchorGravity="bottom|end" />

</androidx.coordinatorlayout.widget.CoordinatorLayout>

這些佈局都沒啥好說的。
NestedScrollView和ScrollView類似。ScrollView它允許使用滾動的方式來檢視螢幕以外的資料,而NestedScrollView在此基礎之上還增加了巢狀響應滾動事件的功能。由於CoordinatorLayout本身已經可以響應滾動事件了,因此我們在它的內部就需要使用NestedScrollView或RecyclerView這樣的佈局。NestedScrollView和ScrollView一樣,內部只能存在一個直接子佈局,所以我們一般新增一個LinearLayout的巢狀。
FloatingActionButton中的app:layout_anchor和app:layout_anchorGravity相當於一個錨點,來確定FloatingActionButton的位置。

public class FruitActivity extends AppCompatActivity {

    public static final String FRUIT_NAME = "fruit_name";

    public 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 = findViewById(R.id.toolbar);
        CollapsingToolbarLayout collapsingToolbar = findViewById(R.id.collapsing_toolbar);
        ImageView fruitImageView = findViewById(R.id.fruit_image_view);
        TextView fruitContentText = findViewById(R.id.fruit_content_text);
        setSupportActionBar(toolbar);
        ActionBar actionBar = getSupportActionBar();
        if (actionBar != null) {
            actionBar.setDisplayHomeAsUpEnabled(true);
        }
        collapsingToolbar.setTitle(fruitName);
        Glide.with(this).load(fruitImageId).into(fruitImageView);
        String fruitContent = generateFruitContent(fruitName);
        fruitContentText.setText(fruitContent);
    }

    private String generateFruitContent(String fruitName) {
        StringBuilder fruitContent = new StringBuilder();
        for (int i = 0; i < 500; i++) {
            fruitContent.append(fruitName);
        }
        return fruitContent.toString();
    }

    @Override
    public boolean onOptionsItemSelected(MenuItem item) {
        switch (item.getItemId()) {
            case android.R.id.home:
                finish();
                return true;
        }
        return super.onOptionsItemSelected(item);
    }
}

然後就是FruitActivity,沒什麼好說的,之後通過MainActivity中的recyclerview中的item跳轉過來就完事了。

充分利用系統狀態列控制元件

第一步:

——第一行程式碼
想要讓背景圖能夠和系統狀態列融合,需要藉助android:fitsSystemWindows這個屬性來實現。在oordinatorLayout、AppBarLayout、CollapsingToolbarLayout這種巢狀結構的佈局中,將控制元件的ndroid:fitsSystemWindows屬性指定成true,就表示該控制元件會出現在系統狀態列裡。對應到我們的程式,就是水果標題欄中的ImageView應該設定這個屬性了。不過只給ImageView設定這個屬性是沒有用的,我們必須將ImageView佈局結構中的所有父佈局都設定上這個屬性才可以。

第二步:相容安卓5.0,設定透明背景主題

——第一行程式碼
但是,即使我們將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="AppTheme" >
        <item name="android:statusBarColor">@android:color/transparent</item>
    </style>
</resources>

第三步:修改value/styles.xml

<style name="FruitActivityTheme" parent="AppTheme">
    </style>

新增以上程式碼就可以了

第四步:將FruitActivity載入主題
修改AndroidManifest中的相關部分,如下:

  <activity android:name=".FruitActivity"
            android:theme="@style/FruitActivityTheme">
        </activity>

總結

這一章節的內容非常有趣,而且可以讓你的App顏值和互動大大提升,但是有些東西應該做到能用,會用。而不是去死記硬背,這也是程式設計的通理,因為它永遠只是一個控制元件,和它一樣的控制元件還有很多很多。筆者一開始就犯了一個很大的錯誤,認為這些都不能信手拈來,以後怎麼做一名高階開發。誠然,真正高階的開發人員往往會把精力花費在鑽研底層,而不是使用。當擁有雄厚的內力時,想要用好控制元件,只需要上網瀏覽一下相關的開發文件,就可以快速上手。
所以會閱讀文件也是十分關鍵,這裡是谷歌Material的官方文件,有興趣的朋友可以看看。
https://material.google.com