主頁 > 移動端開發 > 《第一行代碼:Android篇》學習筆記(十二)

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

2022-05-12 08:34:06 移動端開發

本文和接下來的幾篇文章為閱讀郭霖先生所著《第一行代碼: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_
    android:layout_height="match_parent">
    <androidx.appcompat.widget.Toolbar
        android:layout_
        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_
    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>
    <TextView
        android:layout_
        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_
    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左側的導航按鈕,效果如圖:

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_
    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設定了一個圖示,

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

image

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

仔細觀察的話,會發現這個懸浮按鈕的下面還有一點陰影,因為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,效果如圖:

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_
    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,因此這種替換不會有任何的副作用,現在重新運行一下程式,并點擊懸浮按鈕,效果如圖:

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_
        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,第二個是列數,這里我們希望每一行中會有兩列資料,現在重新運行一下程式,效果如圖:

image

水果圖片成功展示出來了,每個水果都是在一張單獨的卡片當中的,并且還擁有圓角和投影,另外,由于我們是使用隨機的方式來獲取水果資料的,因此界面上會有一些重復的水果出現,這屬于正常現象,

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庫提供的,現在重新運行一下程式,如圖:

image

雖說使用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,效果如圖:

image

可以看到,隨著我們向上滾動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>

image

在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,用于表示重繪事件結束,并隱藏重繪進度條,

現在可以重新運行一下程式了,在螢屏的主界面向下拖動,會有一個下拉重繪的進度條出現,松手后就會自動進行重繪了,效果如圖:

image

下拉重繪的進度條只會停留兩秒鐘,之后就會自動消失,界面上的水果資料也會隨之更新,這樣我們就把下拉重繪的功能也成功實作了,

12.7 可折疊式標題欄

雖說我們現在的標題欄是使用Toolbar來撰寫的,不過它看上去和傳統的ActionBar其實沒什么兩樣,只不過可以回應RecyclerView的滾動事件來進行隱藏和顯示,而Material Design中并沒有限定標題欄必須是長這個樣子的,事實上,我們可以根據自己的喜好隨意定制標題欄的樣式,那么本節中我們就來實作一個可折疊式標題欄的效果,需要借助CollapsingToolbarLayout這個工具,

12.7.1 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,

現在重新運行一下程式,并點擊界面上的任意一個水果,比如我點擊了橘子,效果如圖:

image

這個界面上的內容分為三部分,水果標題欄、水果內容詳情和懸浮按鈕,

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

image

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

image

可以看到,標題欄的背景圖片不見了,懸浮按鈕也自動消失了,現在水果標題欄變成了一個最普通的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程式,水果詳情展示界面的效果就會如圖:

image

12.8 小結與點評

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

個人學習筆記,針對本人在自學中遇到的問題,

轉載請註明出處,本文鏈接:https://www.uj5u.com/yidong/472412.html

標籤:Android

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

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

標籤雲
其他(157675) Python(38076) JavaScript(25376) Java(17977) C(15215) 區塊鏈(8255) C#(7972) AI(7469) 爪哇(7425) MySQL(7132) html(6777) 基礎類(6313) sql(6102) 熊猫(6058) PHP(5869) 数组(5741) R(5409) Linux(5327) 反应(5209) 腳本語言(PerlPython)(5129) 非技術區(4971) Android(4554) 数据框(4311) css(4259) 节点.js(4032) C語言(3288) json(3245) 列表(3129) 扑(3119) C++語言(3117) 安卓(2998) 打字稿(2995) VBA(2789) Java相關(2746) 疑難問題(2699) 细绳(2522) 單片機工控(2479) iOS(2429) ASP.NET(2402) MongoDB(2323) 麻木的(2285) 正则表达式(2254) 字典(2211) 循环(2198) 迅速(2185) 擅长(2169) 镖(2155) 功能(1967) .NET技术(1958) Web開發(1951) python-3.x(1918) HtmlCss(1915) 弹簧靴(1913) C++(1909) xml(1889) PostgreSQL(1872) .NETCore(1853) 谷歌表格(1846) Unity3D(1843) for循环(1842)

熱門瀏覽
  • 【從零開始擼一個App】Dagger2

    Dagger2是一個IOC框架,一般用于Android平臺,第一次接觸的朋友,一定會被搞得暈頭轉向。它延續了Java平臺Spring框架代碼碎片化,注解滿天飛的傳統。嘗試將各處代碼片段串聯起來,理清思緒,真不是件容易的事。更不用說還有各版本細微的差別。 與Spring不同的是,Spring是通過反射 ......

    uj5u.com 2020-09-10 06:57:59 more
  • Flutter Weekly Issue 66

    新聞 Flutter 季度調研結果分享 教程 Flutter+FaaS一體化任務編排的思考與設計 詳解Dart中如何通過注解生成代碼 GitHub 用對了嗎?Flutter 團隊分享如何管理大型開源專案 插件 flutter-bubble-tab-indicator A Flutter librar ......

    uj5u.com 2020-09-10 06:58:52 more
  • Proguard 常用規則

    介紹 Proguard 入口,如何查看輸出,如何使用 keep 設定入口以及使用實體,如何配置壓縮,混淆,校驗等規則。

    ......

    uj5u.com 2020-09-10 06:59:00 more
  • Android 開發技術周報 Issue#292

    新聞 Android即將獲得類AirDrop功能:可向附近設備快速分享檔案 谷歌為安卓檔案管理應用引入可安全隱藏資料的Safe Folder功能 Android TV新主界面將顯示電影、電視節目和應用推薦內容 泄露的Android檔案暗示了傳說中的谷歌Pixel 5a與折疊屏新機 谷歌發布Andro ......

    uj5u.com 2020-09-10 07:00:37 more
  • AutoFitTextureView Error inflating class

    報錯: Binary XML file line #0: Binary XML file line #0: Error inflating class xxx.AutoFitTextureView 解決: <com.example.testy2.AutoFitTextureView android: ......

    uj5u.com 2020-09-10 07:00:41 more
  • 根據Uri,Cursor沒有獲取到對應的屬性

    Android: 背景:呼叫攝像頭,拍攝視頻,指定保存的地址,但是回傳的Cursor檔案,只有名稱和大小的屬性,沒有其他諸如時長,連ID屬性都沒有 使用 cursor.getInt(cursor.getColumnIndexOrThrow(MediaStore.Video.Media.DURATIO ......

    uj5u.com 2020-09-10 07:00:44 more
  • Android連載29-持久化技術

    一、持久化技術 我們平時所使用的APP產生的資料,在記憶體中都是瞬時的,會隨著斷電、關機等丟失資料,因此android系統采用了持久化技術,用于存盤這些“瞬時”資料 持久化技術包括:檔案存盤、SharedPreference存盤以及資料庫存盤,還有更復雜的SD卡記憶體儲。 二、檔案存盤 最基本存盤方式, ......

    uj5u.com 2020-09-10 07:00:47 more
  • Android Camera2Video整合到自己專案里

    背景: Android專案里呼叫攝像頭拍攝視頻,原本使用的 MediaStore.ACTION_VIDEO_CAPTURE, 后來因專案需要,改成了camera2 1.Camera2Video 官方demo有點問題,下載后,不能直接整合到專案 問題1.多次拍攝視頻崩潰 問題2.雙擊record按鈕, ......

    uj5u.com 2020-09-10 07:00:50 more
  • Android 開發技術周報 Issue#293

    新聞 谷歌為Android TV開發者提供多種新功能 Android 11將自動填表功能整合到鍵盤輸入建議中 谷歌宣布Android Auto即將支持更多的導航和數字停車應用 谷歌Pixel 5只有XL版本 搭載驍龍765G且將比Pixel 4更便宜 [圖]Wear OS將迎來重磅更新:應用啟動時間 ......

    uj5u.com 2020-09-10 07:01:38 more
  • 海豚星空掃碼投屏 Android 接收端 SDK 集成 六步驟

    掃碼投屏,開放網路,獨占設備,不需要額外下載軟體,微信掃碼,發現設備。支持標準DLNA協議,支持倍速播放。視頻,音頻,圖片投屏。好點意思。還支持自定義基于 DLNA 擴展的操作動作。好像要收費,沒體驗。 這里簡單記錄一下集成程序。 一 跟目錄的build.gradle添加私有mevan倉庫 mave ......

    uj5u.com 2020-09-10 07:01:43 more
最新发布
  • 歡迎頁輪播影片

    如圖,引導開始,球從上落下,同時淡入文字,然后文字開始輪播,最后一頁時停止,點擊進入首頁。 在來看看效果圖。 重力球先不講,主要歡迎輪播簡單實作 首先新建一個類 TextTranslationXGuideView,用于影片展示 文本是類似的,最后會有個圖片箭頭影片,布局很簡單,就是一個 TextVi ......

    uj5u.com 2023-04-20 08:40:31 more
  • 【FAQ】關于華為推送服務因營銷訊息頻次管控導致服務通訊類訊息

    一. 問題描述 使用華為推送服務下發IM訊息時,下發訊息請求成功且code碼為80000000,但是手機總是收不到訊息; 在華為推送自助分析(Beta)平臺查看發現,訊息發送觸發了頻控。 二. 問題原因及背景 2023年1月05日起,華為推送服務對咨詢營銷類訊息做了單個設備每日推送數量上限管理,具體 ......

    uj5u.com 2023-04-20 08:40:11 more
  • 歡迎頁輪播影片

    如圖,引導開始,球從上落下,同時淡入文字,然后文字開始輪播,最后一頁時停止,點擊進入首頁。 在來看看效果圖。 重力球先不講,主要歡迎輪播簡單實作 首先新建一個類 TextTranslationXGuideView,用于影片展示 文本是類似的,最后會有個圖片箭頭影片,布局很簡單,就是一個 TextVi ......

    uj5u.com 2023-04-20 08:39:36 more
  • 【FAQ】關于華為推送服務因營銷訊息頻次管控導致服務通訊類訊息

    一. 問題描述 使用華為推送服務下發IM訊息時,下發訊息請求成功且code碼為80000000,但是手機總是收不到訊息; 在華為推送自助分析(Beta)平臺查看發現,訊息發送觸發了頻控。 二. 問題原因及背景 2023年1月05日起,華為推送服務對咨詢營銷類訊息做了單個設備每日推送數量上限管理,具體 ......

    uj5u.com 2023-04-20 08:39:13 more
  • iOS從UI記憶體地址到讀取成員變數(oc/swift)

    開發除錯時,我們發現bug時常首先是從UI顯示發現例外,下一步才會去定位UI相關連的資料的。XCode有給我們提供一系列debug工具,但是很多人可能還沒有形成一套穩定的除錯流程,因此本文嘗試解決這個問題,順便提出一個暴論:UI顯示例外問題只需要兩個步驟就能完成定位作業的80%: 定位例外 UI 組 ......

    uj5u.com 2023-04-19 09:16:23 more
  • FIDE重磅更新!性能飛躍!體驗有禮!

    FIDE 開發者工具重構升級啦!實作500%性能提升,誠邀體驗! 一直以來不少開發者朋友在社區反饋,在使用 FIDE 工具的程序中,時常會遇到諸如加載不及時、代碼預覽/渲染性能不如意的情況,十分影響開發體驗。 作為技術團隊,我們深知一件趁手的開發工具對開發者的重要性,因此,在2023年開年,FinC ......

    uj5u.com 2023-04-19 09:16:15 more
  • 游戲內嵌社區服務開放,助力開發者提升玩家互動與留存

    華為 HMS Core 游戲內嵌社區服務提供快速訪問華為游戲中心論壇能力,支持玩家直接在游戲內瀏覽帖子和交流互動,助力開發者擴展內容生產和觸達的場景。 一、為什么要游戲內嵌社區? 二、游戲內嵌社區的典型使用場景 1、游戲內打開論壇 您可以在游戲內繪制論壇入口,為玩家提供沉浸式發帖、瀏覽、點贊、回帖、 ......

    uj5u.com 2023-04-19 09:15:46 more
  • iOS從UI記憶體地址到讀取成員變數(oc/swift)

    開發除錯時,我們發現bug時常首先是從UI顯示發現例外,下一步才會去定位UI相關連的資料的。XCode有給我們提供一系列debug工具,但是很多人可能還沒有形成一套穩定的除錯流程,因此本文嘗試解決這個問題,順便提出一個暴論:UI顯示例外問題只需要兩個步驟就能完成定位作業的80%: 定位例外 UI 組 ......

    uj5u.com 2023-04-19 09:14:53 more
  • FIDE重磅更新!性能飛躍!體驗有禮!

    FIDE 開發者工具重構升級啦!實作500%性能提升,誠邀體驗! 一直以來不少開發者朋友在社區反饋,在使用 FIDE 工具的程序中,時常會遇到諸如加載不及時、代碼預覽/渲染性能不如意的情況,十分影響開發體驗。 作為技術團隊,我們深知一件趁手的開發工具對開發者的重要性,因此,在2023年開年,FinC ......

    uj5u.com 2023-04-19 09:14:08 more
  • 游戲內嵌社區服務開放,助力開發者提升玩家互動與留存

    華為 HMS Core 游戲內嵌社區服務提供快速訪問華為游戲中心論壇能力,支持玩家直接在游戲內瀏覽帖子和交流互動,助力開發者擴展內容生產和觸達的場景。 一、為什么要游戲內嵌社區? 二、游戲內嵌社區的典型使用場景 1、游戲內打開論壇 您可以在游戲內繪制論壇入口,為玩家提供沉浸式發帖、瀏覽、點贊、回帖、 ......

    uj5u.com 2023-04-19 09:08:34 more