1. 程式人生 > 其它 >《第一行程式碼:Android篇》學習筆記(三)

《第一行程式碼:Android篇》學習筆記(三)

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

第3章 軟體也要拼臉蛋——UI開發的點點滴滴

Android也給我們提供了大量的UI開發工具,只要合理地使用它們,就可以編寫出各種各樣漂亮的介面。在這裡,我無法教會你如何提升自己的審美觀,但我可以教會你怎樣使用Android提供的UI開發工具來編寫程式介面。

3.1 如何編寫程式介面

Android中有多種編寫程式介面的方式可供選擇。Android Studio和Eclipse中都提供了相應的視覺化編輯器,允許使用拖放控制元件的方式來編寫佈局,並能在檢視上直接修改控制元件的屬性。

不過我並不推薦你使用這種方式來編寫介面,因為視覺化編輯工具並不利於你去真正瞭解介面背後的實現原理。通過這種方式製作出的介面通常不具有很好的螢幕適配性,而且當需要編寫較為複雜的介面時,視覺化編輯工具將很難勝任。

因此本書中所有的介面都將通過最基本的方式去實現,即編寫XML程式碼。等你完全掌握了使用XML來編寫介面的方法之後,不管是進行高複雜度的介面實現,還是分析和修改當前現有介面,對你來說都將是手到擒來。

下面我們就從Android中幾種常見的控制元件開始吧。

3.2 常用控制元件的使用方法

Android提供了大量的UI控制元件,合理地使用這些控制元件就可以非常輕鬆地編寫出相當不錯的介面,下面就挑選幾種常用的控制元件,詳細介紹一下它們的使用方法。

首先新建一個UIWidgetTest專案,我們還是允許Android Studio自動建立活動,活動名和佈局名都使用預設值。

3.2.1 TextView

修改activity_main.xml中的程式碼,如下所示:

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:orientation="vertical"
    android:layout_width="match_parent"
    android:layout_height="match_parent">

    <TextView
        android:id="@+id/text_view"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:text="This is TextView"/>
</LinearLayout>
  • 在TextView中我們使用android:id給當前控制元件定義了一個唯一識別符號

  • 用android:layout_width和android:layout_height指定了控制元件的寬度和高度

    Android中所有的控制元件都具有這兩個屬性,可選值有3種:match_parent、fill_parent和wrap_content。

    • match_parent和fill_parent的意義相同,現在官方更加推薦使用match_parent。
    • match_parent表示讓當前控制元件的大小和父佈局的大小一樣,也就是由父佈局來決定當前控制元件的大小。
    • wrap_content表示讓當前控制元件的大小能夠剛好包含住裡面的內容,由控制元件內容決定當前控制元件的大小。

    所以,上面的程式碼就表示讓TextView的寬度和父佈局一樣寬,也就是手機螢幕的寬度,讓TextView的高度足夠包含住裡面的內容就行。當然除了使用上述值,你也可以對控制元件的寬和高指定一個固定的大小,但是這樣做有時會在不同手機螢幕的適配方面出現問題。

    接下來我們通過android:text指定TextView中顯示的文字內容,現在執行程式,效果如圖:

由於TextView中的文字預設是居左上角對齊的,雖然TextView的寬度充滿了整個螢幕,可是由於文字內容不夠長,所以從效果上完全看不出來。修改TextView的文字對齊方式,如下所示:

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:orientation="vertical"
    android:layout_width="match_parent"
    android:layout_height="match_parent">

    <TextView
        android:id="@+id/text_view"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:gravity="center"
        android:text="This is TextView"/>
</LinearLayout>

使用android:gravity來指定文字的對齊方式,可選值有top、bottom、left、right、center等,可以用“|”來同時指定多個值,這裡我們指定的center,效果等同於center_vertical|center_horizontal,表示文字在垂直和水平方向都居中對齊。現在重新執行程式,效果如圖:

這也說明了TextView的寬度確實是和螢幕寬度一樣的。另外還可以對TextView中文字的大小和顏色進行修改,如下所示:

 <TextView
        android:id="@+id/text_view"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:gravity="center"
        android:textSize="24sp"
        android:textColor="#00ff00"
        android:text="This is TextView"/>

當然TextView中還有很多其他的屬性,這裡就不再一一介紹了,用到的時候去查閱文件就可以了。

3.2.2 Button

Button是程式用於和使用者進行互動的一個重要控制元件,可以在activity_main.xml中這樣加入Button:

    <Button
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:id="@+id/button"
        android:text="Button"/>

你可能會留意到,我們在佈局檔案裡面設定的文字是“Button”,但最終的顯示結果卻是“BUTTON”。這是由於系統會對Button中的所有英文字母自動進行大寫轉換,如果這不是你想要的效果,可以使用如下配置來禁用這一預設特性:

<Button
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:id="@+id/button"
        android:text="Button"
        android:textAllCaps="false"/>

接下來可以在MainActivity中為Button的點選事件註冊一個監聽器,如下所示:

package com.zhouzhou.uiwidgettest;

import androidx.appcompat.app.AppCompatActivity;

import android.os.Bundle;
import android.view.View;
import android.widget.Button;

public class MainActivity extends AppCompatActivity {

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        Button button = (Button) findViewById(R.id.button);
        button.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View view) {
                //此處新增邏輯
            }
        });
    }
}

每當點選按鈕時,就會執行監聽器中的onClick()方法,我們只需要在這個方法中加入待處理的邏輯就行了。如果你不喜歡使用匿名類的方式來註冊監聽器,也可以使用實現介面的方式來進行註冊,程式碼如下所示:

package com.zhouzhou.uiwidgettest;

import androidx.appcompat.app.AppCompatActivity;

import android.os.Bundle;
import android.view.View;
import android.widget.Button;

public class MainActivity extends AppCompatActivity implements View.OnClickListener{

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        Button button = (Button) findViewById(R.id.button);
        button.setOnClickListener(this);
    }
    //實現View.OnClickListener介面
    @Override
    public void onClick(View v){
        switch (v.getId()){
            case R.id.button:
                //此處新增邏輯
                break;
            default:
                break;
        }
    }
}

3.2.3 EditText

EditText是程式用於和使用者進行互動的另一個重要控制元件,它允許使用者在控制元件裡輸入和編輯內容,並可以在程式中對這些內容進行處理。EditText的應用場景非常普遍,在進行發簡訊、發微博、聊QQ等操作時,使用EditText。修改activity_main.xml中的程式碼,如下所示:

    <EditText
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:id="@+id/edit_text"/>

Android控制元件的使用規律了,用法基本上都很相似:給控制元件定義一個id,再指定控制元件的寬度和高度,然後再適當加入一些控制元件特有的屬性就差不多了。

所以使用XML來編寫介面其實一點都不難,完全可以不用藉助任何視覺化工具來實現。現在重新執行一下程式,EditText就已經在介面上顯示出來了,並且我們是可以在裡面輸入內容的,如圖:

做得比較人性化的軟體會在輸入框裡顯示一些提示性的文字,然後一旦使用者輸入了任何內容,這些提示性的文字就會消失。這種提示功能在Android裡是非常容易實現的,我們甚至不需要做任何的邏輯控制,因為系統已經幫我們都處理好了。修改activity_main.xml,如下所示:

 <EditText
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:id="@+id/edit_text"
        android:hint="新鮮事兒"/>

EditText中顯示了一段提示性文字——“新鮮事兒”,當輸入任何內容時,這段文字就會自動消失。

不過,隨著輸入的內容不斷增多,EditText會被不斷地拉長。這時由於EditText的高度指定的是wrap_content,因此它總能包含住裡面的內容,但是當輸入的內容過多時,介面就會變得非常難看。我們可以使用android:maxLines屬性來解決這個問題,修改activity_main.xml,如下所示:

    <EditText
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:id="@+id/edit_text"
        android:hint="新鮮事兒"
        android:maxLines="2"/>

過android:maxLines指定了EditText的最大行數為兩行,這樣當輸入的內容超過兩行時,文字就會向上滾動,而EditText則不會再繼續拉伸。

還可以結合使用EditText與Button來完成一些功能,比如通過點選按鈕來獲取EditText中輸入的內容。修改MainActivity中的程式碼,如下所示:

package com.zhouzhou.uiwidgettest;

import androidx.appcompat.app.AppCompatActivity;

import android.os.Bundle;
import android.view.View;
import android.widget.Button;
import android.widget.EditText;
import android.widget.Toast;

public class MainActivity extends AppCompatActivity implements View.OnClickListener{

    private EditText editText;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        Button button = (Button) findViewById(R.id.button);
        editText = (EditText) findViewById(R.id.edit_text);

        button.setOnClickListener(this);
    }
    //實現View.OnClickListener介面
    @Override
    public void onClick(View v){
        switch (v.getId()){
            case R.id.button:
                //此處新增邏輯
                String inputText = editText.getText().toString();
                Toast.makeText(MainActivity.this,inputText,Toast.LENGTH_SHORT).show();
                break;
            default:
                break;
        }
    }
}

3.2.4 ImageView

ImageView是用於在介面上展示圖片的一個控制元件,它可以讓我們的程式介面變得更加豐富多彩。

學習這個控制元件需要提前準備好一些圖片,圖片通常都是放在以“drawable”開頭的目錄下的。目前我們的專案中有一個空的drawable目錄,不過由於這個目錄沒有指定具體的解析度,所以一般不使用它來放置圖片。

在res目錄下新建一個drawable-xhdpi目錄,然後將事先準備好的兩張圖片img_1.png和img_2.png複製到該目錄當中。接下來修改activity_main.xml,如下所示:

    <ImageView
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:id="@+id/image_view"
        android:src="@drawable/img_1"/>

這裡使用android:src屬性給ImageView指定了一張圖片。由於圖片的寬和高都是未知的,所以將ImageView的寬和高都設定為wrap_content,這樣就保證了不管圖片的尺寸是多少,圖片都可以完整地展示出來。重新執行程式,效果如圖:

可以在程式中通過程式碼動態地更改ImageView中的圖片,然後修改MainActivity的程式碼,如下所示:

package com.zhouzhou.uiwidgettest;

import androidx.appcompat.app.AppCompatActivity;

import android.os.Bundle;
import android.view.View;
import android.widget.Button;
import android.widget.EditText;
import android.widget.ImageView;
import android.widget.Toast;

public class MainActivity extends AppCompatActivity implements View.OnClickListener{

    private EditText editText;
    private ImageView imageView;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        Button button = (Button) findViewById(R.id.button);
        editText = (EditText) findViewById(R.id.edit_text);
        imageView = (ImageView) findViewById(R.id.image_view);

        button.setOnClickListener(this);
    }
    //實現View.OnClickListener介面
    @Override
    public void onClick(View v){
        switch (v.getId()){
            case R.id.button:
                //此處新增邏輯
                imageView.setImageResource(R.drawable.img_2);
                break;
            default:
                break;
        }
    }
}

在按鈕的點選事件裡,通過呼叫ImageView的setImageResource()方法將顯示的圖片改成img_2,現在重新執行程式,然後點選一下按鈕,就可以看到ImageView中顯示的圖片改變了,如圖:

3.2.5 ProgressBar

ProgressBar用於在介面上顯示一個進度條,表示我們的程式正在載入一些資料。它的用法也非常簡單,修改activity_main.xml中的程式碼,如下所示:

    <ProgressBar
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:id="@+id/progress_bar"/>

重新執行程式,會看到螢幕中有一個圓形進度條正在旋轉:

一個新的知識點:Android控制元件的可見屬性

所有的Android控制元件都具有這個屬性,可以通過android:visibility進行指定,可選值有3種:visible、invisible和gone。

  • visible表示控制元件是可見的,這個值是預設值,不指定android:visibility時,控制元件都是可見的。
  • invisible表示控制元件不可見,但是它仍然佔據著原來的位置和大小,可以理解成控制元件變成透明狀態了。
  • gone則表示控制元件不僅不可見,而且不再佔用任何螢幕空間。

我們還可以通過程式碼來設定控制元件的可見性,使用的是setVisibility()方法,可以傳入View.VISIBLE、View.INVISIBLE和View.GONE這3種值。

接下來我們就來嘗試實現,點選一下按鈕讓進度條消失,再點選一下按鈕讓進度條出現的這種效果。修改MainActivity中的程式碼,如下所示:

package com.zhouzhou.uiwidgettest;

import androidx.appcompat.app.AppCompatActivity;

import android.os.Bundle;
import android.view.View;
import android.widget.Button;
import android.widget.EditText;
import android.widget.ImageView;
import android.widget.ProgressBar;
import android.widget.Toast;

public class MainActivity extends AppCompatActivity implements View.OnClickListener{

    private EditText editText;
    private ImageView imageView;
    private ProgressBar progressBar;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        Button button = (Button) findViewById(R.id.button);
        editText = (EditText) findViewById(R.id.edit_text);
        imageView = (ImageView) findViewById(R.id.image_view);
        progressBar = (ProgressBar) findViewById(R.id.progress_bar);

        button.setOnClickListener(this);
    }
    //實現View.OnClickListener介面
    @Override
    public void onClick(View v){
        switch (v.getId()){
            case R.id.button:
                //此處新增邏輯
                //imageView.setImageResource(R.drawable.img_2);
                if (progressBar.getVisibility() == View.GONE){
                    progressBar.setVisibility(View.VISIBLE);
                }else{
                    progressBar.setVisibility(View.GONE);
                }
                break;
            default:
                break;
        }
    }
}

在按鈕的點選事件中,我們通過getVisibility()方法來判斷ProgressBar是否可見,如果可見就將ProgressBar隱藏掉,如果不可見就將ProgressBar顯示出來。

重新執行程式,然後不斷地點選按鈕,你就會看到進度條會在顯示與隱藏之間來回切換。另外,我們還可以給ProgressBar指定不同的樣式,剛剛是圓形進度條,通過style屬性可以將它指定成水平進度條,修改activity_main.xml中的程式碼,如下所示:

    <ProgressBar
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:id="@+id/progress_bar"
        style="?android:attr/progressBarStyleHorizontal"
        android:max="100"/>

指定成水平進度條後,我們還可以通過android:max屬性給進度條設定一個最大值,然後在程式碼中動態地更改進度條的進度。修改MainActivity中的程式碼,如下所示:

package com.zhouzhou.uiwidgettest;

import androidx.appcompat.app.AppCompatActivity;

import android.os.Bundle;
import android.view.View;
import android.widget.Button;
import android.widget.EditText;
import android.widget.ImageView;
import android.widget.ProgressBar;
import android.widget.Toast;

public class MainActivity extends AppCompatActivity implements View.OnClickListener{

    private ProgressBar progressBar;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        Button button = (Button) findViewById(R.id.button);
        progressBar = (ProgressBar) findViewById(R.id.progress_bar);

        button.setOnClickListener(this);
    }
    @Override
    public void onClick(View v){
        switch (v.getId()){
            case R.id.button:
                //此處新增邏輯
                int progress = progressBar.getProgress();
                progress = progress + 10;
                progressBar.setProgress(progress);
                break;
            default:
                break;
        }
    }
}

每點選一次按鈕,我們就獲取進度條的當前進度,然後在現有的進度上加10作為更新後的進度。重新執行程式,點選數次按鈕後,效果如圖:

ProgressBar還有幾種其他的樣式,你可以自己去嘗試一下。

3.2.6 AlertDialog

AlertDialog可以在當前的介面彈出一個對話方塊,這個對話方塊是置頂於所有介面元素之上的,能夠遮蔽掉其他控制元件的互動能力,因此AlertDialog一般都是用於提示一些非常重要的內容或者警告資訊。

比如為了防止使用者誤刪重要內容,在刪除前彈出一個確認對話方塊。修改MainActivity中的程式碼,如下所示:

package com.zhouzhou.uiwidgettest;

import androidx.appcompat.app.AlertDialog;
import androidx.appcompat.app.AppCompatActivity;

import android.content.DialogInterface;
import android.os.Bundle;
import android.view.View;
import android.widget.Button;
import android.widget.EditText;
import android.widget.ImageView;
import android.widget.ProgressBar;
import android.widget.Toast;

public class MainActivity extends AppCompatActivity implements View.OnClickListener{

    private EditText editText;
    private ImageView imageView;
    private ProgressBar progressBar;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        Button button = (Button) findViewById(R.id.button);
        editText = (EditText) findViewById(R.id.edit_text);
        imageView = (ImageView) findViewById(R.id.image_view);
        progressBar = (ProgressBar) findViewById(R.id.progress_bar);

        button.setOnClickListener(this);
    }
    //實現View.OnClickListener介面
    @Override
    public void onClick(View v){
        switch (v.getId()){
            case R.id.button:
                //此處新增邏輯
                AlertDialog.Builder dialog= new AlertDialog.Builder(MainActivity.this);
                dialog.setTitle("This is Dialog");
                dialog.setMessage("Something important.");
                dialog.setCancelable(false);//可否用Back鍵關閉對話方塊
                dialog.setPositiveButton("OK", new DialogInterface.OnClickListener() {
                    @Override
                    public void onClick(DialogInterface dialogInterface, int i) {
                    }
                });
                dialog.setNegativeButton("Cancel", new DialogInterface.OnClickListener() {
                    @Override
                    public void onClick(DialogInterface dialogInterface, int i) {
                    }
                });
                dialog.show();
                break;
            default:
                break;
        }
    }
}

首先通過AlertDialog.Builder建立一個AlertDialog的例項,然後可以為這個對話方塊設定標題、內容、可否用Back鍵關閉對話方塊等屬性,接下來呼叫setPositiveButton()方法為對話方塊設定確定按鈕的點選事件,呼叫setNegativeButton()方法設定取消按鈕的點選事件,最後呼叫show()方法將對話方塊顯示出來。

重新執行程式,點選按鈕後,效果如圖所示:

3.2.7 ProgressDialog

ProgressDialog和AlertDialog有點類似,都可以在介面上彈出一個對話方塊,都能夠遮蔽掉其他控制元件的互動能力。不同的是,ProgressDialog會在對話方塊中顯示一個進度條,一般用於表示當前操作比較耗時,讓使用者耐心地等待。它的用法和AlertDialog也比較相似,修改MainActivity中的程式碼,如下所示:

package com.zhouzhou.uiwidgettest;

import androidx.appcompat.app.AppCompatActivity;

import android.app.ProgressDialog;
import android.os.Bundle;
import android.view.View;
import android.widget.Button;

public class MainActivity extends AppCompatActivity implements View.OnClickListener{
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        Button button = (Button) findViewById(R.id.button);
        button.setOnClickListener(this);
    }
    @Override
    public void onClick(View v){
        switch (v.getId()){
            case R.id.button:
                //此處新增邏輯
                ProgressDialog progressDialog = new ProgressDialog(MainActivity.this);
                progressDialog.setTitle("This is ProgressDialog");
                progressDialog.setMessage("Loading...");
                progressDialog.setCancelable(true);
                progressDialog.show();

                break;
            default:
                break;
        }
    }
}

可以看到,這裡也是先構建出一個ProgressDialog物件,然後同樣可以設定標題、內容、可否取消等屬性,最後也是通過呼叫show()方法將ProgressDialog顯示出來。重新執行程式,點選按鈕後,效果如圖:

注意:如果在setCancelable()中傳入了false,表示ProgressDialog是不能通過Back鍵取消掉的,這時你就一定要在程式碼中做好控制,當資料載入完成後必須要呼叫ProgressDialog的dismiss()方法來關閉對話方塊,否則ProgressDialog將會一直存在。控制元件先學習這麼多,閱讀文件瞭解更多控制元件用法。

3.3 詳解4種基本佈局

一個豐富的介面總是要由很多個控制元件組成的,那我們如何才能讓各個控制元件都有條不紊地擺放在介面上,而不是亂糟糟的呢?這就需要藉助佈局來實現了。

佈局是一種可用於放置很多控制元件的容器,它可以按照一定的規律調整內部控制元件的位置,從而編寫出精美的介面。當然,佈局的內部除了放置控制元件外,也可以放置佈局,通過多層佈局的巢狀,我們就能夠完成一些比較複雜的介面實現,圖:

下面詳細講解下Android中4種最基本的佈局。

先做好準備工作,新建一個UILayoutTest專案,並讓Android Studio自動幫我們建立好活動,活動名和佈局名都使用預設值。

3.3.1 線性佈局

LinearLayout又稱作線性佈局,是一種非常常用的佈局。

正如它的名字所描述的一樣,這個佈局會將它所包含的控制元件線上性方向上依次排列。在上一節中學習控制元件用法時,所有的控制元件就都是放在LinearLayout佈局裡的,因此上一節中的控制元件也確實是在垂直方向上線性排列的。

既然是線性排列,肯定就不僅只有一個方向,那為什麼上一節中的控制元件都是在垂直方向排列的呢?這是由於我們通過android:orientation屬性指定了排列方向是vertical,如果指定的是horizontal,控制元件就會在水平方向上排列了。下面我們通過實戰來體會一下,修改activity_main.xml中的程式碼,如下所示:

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:orientation="vertical"
    android:layout_width="match_parent"
    android:layout_height="match_parent">
    <Button
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:id="@+id/button1"
        android:text="button1"/>
    <Button
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:id="@+id/button2"
        android:text="button2"/>
    <Button
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:id="@+id/button3"
        android:text="button3"/>

</LinearLayout>

在LinearLayout中添加了3個Button,每個Button的長和寬都是wrap_content,並指定了排列方向是vertical。現在執行一下程式,效果如圖:

然後我們修改一下LinearLayout的排列方向,如下所示:

android:orientation="horizontal"

將android:orientation屬性的值改成了horizontal,這就意味著要讓LinearLayout中的控制元件在水平方向上依次排列。當然如果不指定android:orientation屬性的值,預設的排列方向就是horizontal。重新執行一下程式,效果如圖:

這裡需要注意,如果LinearLayout的排列方向是horizontal,內部的控制元件就絕對不能將寬度指定為match_parent,因為這樣的話,單獨一個控制元件就會將整個水平方向佔滿,其他的控制元件就沒有可放置的位置了。同樣的道理,如果LinearLayout的排列方向是vertical,內部的控制元件就不能將高度指定為match_parent。

首先來看android:layout_gravity屬性,它和上一節中學到的android:gravity屬性看起來有些相似,這兩個屬性有什麼區別呢?

android:gravity用於指定文字在控制元件中的對齊方式,而android:layout_gravity用於指定控制元件在佈局中的對齊方式。android:layout_gravity的可選值和android:gravity差不多。

但是需要注意,當LinearLayout的排列方向是horizontal時,只有垂直方向上的對齊方式才會生效,因為此時水平方向上的長度是不固定的,每新增一個控制元件,水平方向上的長度都會改變,因而無法指定該方向上的對齊方式。同樣的道理,當LinearLayout的排列方向是vertical時,只有水平方向上的對齊方式才會生效。修改activity_main.xml中的程式碼,如下所示:

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:orientation="horizontal"
    android:layout_width="match_parent"
    android:layout_height="match_parent">
    <Button
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:id="@+id/button1"
        android:text="button1"
        android:layout_gravity="top"/>
    <Button
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:id="@+id/button2"
        android:text="button2"
        android:layout_gravity="center_vertical"/>
    <Button
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:id="@+id/button3"
        android:text="button3"
        android:layout_gravity="bottom"/>

</LinearLayout>

由於目前LinearLayout的排列方向是horizontal,因此我們只能指定垂直方向上的排列方向,將第一個Button的對齊方式指定為top,第二個Button的對齊方式指定為center_vertical,第三個Button的對齊方式指定為bottom。重新執行程式,效果如圖:

LinearLayout中的另一個重要屬性——android:layout_weight。

這個屬性允許我們使用比例的方式來指定控制元件的大小,它在手機螢幕的適配性方面可以起到非常重要的作用。比如我們正在編寫一個訊息傳送介面,需要一個文字編輯框和一個傳送按鈕,修改activity_main.xml中的程式碼,如下所示:

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:orientation="horizontal"
    android:layout_width="match_parent"
    android:layout_height="match_parent">
    <EditText
        android:id="@+id/input_message"
        android:layout_width="0dp"
        android:layout_height="wrap_content"
        android:layout_weight="1"
        android:hint="Type something"
        />
    <Button
        android:id="@+id/send"
        android:layout_width="0dp"
        android:layout_height="wrap_content"
        android:layout_weight="1"
        android:text="Send"/>
</LinearLayout>

將EditText和Button的寬度都指定成了0dp,由於我們使用了android:layout_weight屬性,此時控制元件的寬度就不應該再由android:layout_width來決定,這裡指定成0dp是一種比較規範的寫法。

另外,dp是Android中用於指定控制元件大小、間距等屬性的單位,後面還會經常用到它。然後在EditText和Button裡都將android:layout_weight屬性的值指定為1,這表示EditText和Button將在水平方向平分寬度。

然後在EditText和Button裡都將android:layout_weight屬性的值指定為1,這表示EditText和Button將在水平方向平分寬度。

為什麼將android:layout_weight屬性的值同時指定為1就會平分螢幕寬度呢?

系統會先把LinearLayout下所有控制元件指定的layout_weight值相加,得到一個總值,然後每個控制元件所佔大小的比例就是用該控制元件的layout_weight值除以剛才算出的總值。因此如果想讓EditText佔據螢幕寬度的3/5, Button佔據螢幕寬度的2/5,只需要將EditText的layout_weight改成3, Button的layout_weight改成2就可以了。

重新執行程式,你會看到如圖的效果:

還可以通過指定部分控制元件的layout_weight值來實現更好的效果。修改activity_main. xml中的程式碼,如下所示:

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:orientation="horizontal"
    android:layout_width="match_parent"
    android:layout_height="match_parent">
    <EditText
        android:id="@+id/input_message"
        android:layout_width="0dp"
        android:layout_height="wrap_content"
        android:layout_weight="1"
        android:hint="Type something"
        />
    <Button
        android:id="@+id/send"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="Send"/>

</LinearLayout>

這裡僅指定了EditText的android:layout_weight屬性,並將Button的寬度改回wrap_content。這表示Button的寬度仍然按照wrap_content來計算,而EditText則會佔滿螢幕所有的剩餘空間。(

使用layout_weight實現寬度自適配效果,這種方式編寫的介面,不僅在各種螢幕的適配方面會非常好,而且看起來也更加舒服。重新執行程式,效果如圖:

3.3.2 相對佈局

RelativeLayout又稱作相對佈局,也是一種非常常用的佈局。和LinearLayout的排列規則不同,RelativeLayout顯得更加隨意一些,它可以通過相對定位的方式讓控制元件出現在佈局的任何位置。

也正因為如此,RelativeLayout中的屬性非常多,不過這些屬性都是有規律可循的,修改activity_main.xml中的程式碼,如下所示:

<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent">
    <Button
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:id="@+id/button1"
        android:layout_alignParentLeft="true"
        android:layout_alignParentTop="true"
        android:text="Button1"/>
    <Button
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:id="@+id/button2"
        android:layout_alignParentTop="true"
        android:layout_alignParentRight="true"
        android:text="Button2"/>
    <Button
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:id="@+id/button3"
        android:layout_centerInParent="true"
        android:text="Button3"/>
    <Button
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:id="@+id/button4"
        android:layout_alignParentBottom="true"
        android:layout_alignParentLeft="true"
        android:text="Button4"/>
    <Button
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:id="@+id/button5"
        android:layout_alignParentBottom="true"
        android:layout_alignParentRight="true"
        android:text="Button5"/>
</RelativeLayout>

讓Button 1和父佈局的左上角對齊,Button 2和父佈局的右上角對齊,Button3居中顯示,Button 4和父佈局的左下角對齊,Button 5和父佈局的右下角對齊。雖然android:layout_alignParentLeft、android:layout_alignParentTop、android:layout_alignParentRight、android:layout_alignParentBottom、android:layout_centerInParent這幾個屬性它們的名字已經完全說明了它們的作用。

重新執行程式,效果如圖:

上面例子中的每個控制元件都是相對於父佈局進行定位的,那控制元件可不可以相對於控制元件進行定位呢?當然是可以的,修改activity_main.xml中的程式碼,如下所示:

<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent">
    <Button
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:id="@+id/button3"
        android:layout_centerInParent="true"
        android:text="Button3"/>
    <Button
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:id="@+id/button1"
        android:layout_above="@+id/button3"
        android:layout_toLeftOf="@+id/button3"
        android:text="Button1"/>
    <Button
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:id="@+id/button2"
        android:layout_above="@+id/button3"
        android:layout_toRightOf="@+id/button3"
        android:text="Button2"/>
    <Button
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:id="@+id/button4"
        android:layout_below="@+id/button3"
        android:layout_toLeftOf="@+id/button3"
        android:text="Button4"/>
    <Button
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:id="@+id/button5"
        android:layout_below="@id/button3"
        android:layout_toRightOf="@id/button3"
        android:text="Button5"/>
</RelativeLayout>

android:layout_above屬性可以讓一個控制元件位於另一個控制元件的上方,需要為這個屬性指定相對控制元件id的引用,這裡我們填入了@id/button3,表示讓該控制元件位於Button 3的上方。其他的屬性也都是相似的,android:layout_below表示讓一個控制元件位於另一個控制元件的下方,android:layout_toLeftOf表示讓一個控制元件位於另一個控制元件的左側,android:layout_toRightOf表示讓一個控制元件位於另一個控制元件的右側。

注意,當一個控制元件去引用另一個控制元件的id時,該控制元件一定要定義在引用控制元件的後面,不然會出現找不到id的情況。重新執行程式,效果如圖:

RelativeLayout中還有另外一組相對於控制元件進行定位的屬性,android:layout_alignLeft表示讓一個控制元件的左邊緣和另一個控制元件的左邊緣對齊,android:layout_alignRight表示讓一個控制元件的右邊緣和另一個控制元件的右邊緣對齊。此外,還有android:layout_alignTop和android:layout_alignBottom,道理都是一樣的。

3.3.3 幀佈局

FrameLayout又稱作幀佈局,它相比於前面兩種佈局就簡單太多了,因此它的應用場景也少了很多。這種佈局沒有方便的定位方式,所有的控制元件都會預設擺放在佈局的左上角。讓我們通過例子來看一看吧,修改activity_main.xml中的程式碼,如下所示:

<?xml version="1.0" encoding="utf-8"?>
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent">
    <TextView
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:id="@+id/text_view"
        android:text="This is TextView"/>
    <ImageView
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:id="@+id/image_view"
        android:src="@mipmap/ic_launcher"/>
</FrameLayout>

FrameLayout中只是放置了一個TextView和一個ImageView。需要注意的是,當前專案我們沒有準備任何圖片,所以這裡ImageView直接使用了@mipmap來訪問ic_launcher這張圖,雖說這種用法的場景可能非常少,但我還是要告訴你,這是完全可行的。重新執行程式,效果如圖所示:

可以看到,文字和圖片都是位於佈局的左上角。由於ImageView是在TextView之後新增的,因此圖片壓在了文字的上面。除了這種預設效果之外,還可以使用layout_gravity屬性來指定控制元件在佈局中的對齊方式,這和LinearLayout中的用法是相似的。修改activity_main.xml中的程式碼,如下所示:

<?xml version="1.0" encoding="utf-8"?>
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent">
    <TextView
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:id="@+id/text_view"
        android:layout_gravity="left"
        android:text="This is TextView"/>
    <ImageView
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:id="@+id/image_view"
        android:layout_gravity="right"
        android:src="@mipmap/ic_launcher"/>
</FrameLayout>

指定TextView在FrameLayout中居左對齊,指定ImageView在FrameLayout中居右對齊,然後重新執行程式,效果如圖:

總體來講,FrameLayout由於定位方式的欠缺,導致它的應用場景也比較少,不過在下一章中介紹碎片的時候我們還是可以用到它的。

3.3.4 百分比佈局

前面介紹的3種佈局都是從Android 1.0版本中就開始支援了,一直沿用到現在,可以說是滿足了絕大多數場景的介面設計需求。

不過你會發現,只有LinearLayout支援使用layout_weight屬性來實現按比例指定控制元件大小的功能,其他兩種佈局都不支援。

比如說,如果想用RelativeLayout來實現讓兩個按鈕平分佈局寬度的效果,則是比較困難的。為此,Android引入了一種全新的佈局方式來解決此問題——百分比佈局。在這種佈局中,我們可以不再使用wrap_content、match_parent等方式來指定控制元件的大小,而是允許直接指定控制元件在佈局中所佔的百分比,這樣的話就可以輕鬆實現平分佈局甚至是任意比例分割佈局的效果了。

由於LinearLayout本身已經支援按比例指定控制元件的大小了,因此百分比佈局只為FrameLayout和RelativeLayout進行了功能擴充套件,提供了PercentFrameLayout和PercentRelativeLayout這兩個全新的佈局,下面我們就來具體學習一下。

不同於前3種佈局,百分比佈局屬於新增佈局,那麼怎麼才能做到讓新增佈局在所有Android版本上都能使用呢?為此,Android團隊將百分比佈局定義在了support庫當中,我們只需要在專案的build.gradle中新增百分比佈局庫的依賴,就能保證百分比佈局在Android所有系統版本上的相容性了。開啟app/build.gradle檔案,在dependencies閉包中新增如下內容:

dependencies {

    implementation 'androidx.appcompat:appcompat:1.3.0'
    implementation 'com.google.android.material:material:1.4.0'
    implementation 'androidx.constraintlayout:constraintlayout:2.0.4'

    androidTestImplementation 'androidx.test.ext:junit:1.1.3'
    androidTestImplementation 'androidx.test.espresso:espresso-core:3.4.0'

    implementation fileTree(dir: 'libs', include: ['*.jar'])
    implementation 'androidx.appcompat:appcompat:1.0.0'
    implementation 'androidx.percentlayout:percentlayout:1.0.0'
    testImplementation 'junit:junit:4.13.2'
}

需要注意的是,每當修改了任何gradle檔案時,Android Studio都會彈出一個如圖:

這個提示告訴我們,gradle檔案自上次同步之後又發生了變化,需要再次同步才能使專案正常工作。這裡只需要點選Sync Now就可以了,然後gradle會開始進行同步,把我們新新增的百分比佈局庫引入到專案當中。接下來修改activity_main.xml中的程式碼,如下所示:

<?xml version="1.0" encoding="utf-8"?>
<androidx.percentlayout.widget.PercentFrameLayout
    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">
    <Button
        android:id="@+id/button1"
        android:text="Button1"
        android:layout_gravity="right|top"
        app:layout_widthPercent="50%"
        app:layout_heightPercent="50%"/>
    <Button
        android:id="@+id/button2"
        android:text="Button2"
        android:layout_gravity="left|bottom"
        app:layout_widthPercent="50%"
        app:layout_heightPercent="50%"/>
    <Button
        android:id="@+id/button3"
        android:text="Button3"
        android:layout_gravity="right|bottom"
        app:layout_widthPercent="50%"
        app:layout_heightPercent="50%"/>
    <Button
        android:id="@+id/button4"
        android:text="Button4"
        android:layout_gravity="left|top"
        app:layout_widthPercent="50%"
        app:layout_heightPercent="50%" />
</androidx.percentlayout.widget.PercentFrameLayout>

最外層我們使用了PercentFrameLayout,由於百分比佈局並不是內建在系統SDK當中的,所以需要把完整的包路徑寫出來。然後還必須定義一個app的名稱空間,這樣才能使用百分比佈局的自定義屬性。

在PercentFrameLayout中我們定義了4個按鈕,使用app:layout_widthPercent屬性將各按鈕的寬度指定為佈局的50%,使用app:layout_heightPercent屬性將各按鈕的高度指定為佈局的50%。這裡之所以能使用app字首的屬性就是因為剛才定義了app的名稱空間,當然我們一直能使用android字首的屬性也是同樣的道理。

不過PercentFrameLayout還是會繼承FrameLayout的特性,即所有的控制元件預設都是擺放在佈局的左上角。那麼為了讓這4個按鈕不會重疊,這裡還是藉助了layout_gravity來分別將這4個按鈕放置在佈局的左上、右上、左下、右下4個位置。

現在我們已經可以重新執行程式了,不過如果你使用的是老版本的AndroidStudio,可能會在activity_main.xml中看到一些錯誤提示:

這是因為老版本的Android Studio中內建了佈局的檢查機制,認為每一個控制元件都應該通過android:layout_width和android:layout_height屬性指定寬高才是合法的。而其實我們是通過app:layout_widthPercent和app:layout_heightPercent屬性來指定寬高的,所以Android Studio沒檢測到。不過這個錯誤提示並不影響程式執行,直接忽視就可以了。

當然最新的Android Studio 2.2版本中已經修復了這個問題,因此你可能並不會看到上述的錯誤提示。現在重新執行程式,效果如圖所示:

可以看到,每一個按鈕的寬和高都佔據了佈局的50%,這樣我們就輕鬆實現了4個按鈕平分螢幕的效果。另外一個PercentRelativeLayout的用法也是非常相似的,它繼承了RelativeLayout中的所有屬性,並且可以使用app:layout_widthPercent和app:layout_heightPercent來按百分比指定控制元件的寬高,最常用的幾種佈局都講解完了,其實Android中還有AbsoluteLayout、TableLayout等佈局,不過使用得實在是太少了。

3.4 系統控制元件不夠用?建立自定義控制元件

在前面兩節已經學習了Android中的一些常用控制元件以及基本佈局的用法,不過當時我們並沒有關注這些控制元件和佈局的繼承結構:

可以看到,所用的所有控制元件都是直接或間接繼承自View的,所用的所有佈局都是直接或間接繼承自ViewGroup的。

View是Android中最基本的一種UI元件,它可以在螢幕上繪製一塊矩形區域,並能響應這塊區域的各種事件,因此,我們使用的各種控制元件其實就是在View的基礎之上又添加了各自特有的功能。而ViewGroup則是一種特殊的View,它可以包含很多子View和子ViewGroup,是一個用於放置控制元件和佈局的容器。

思考:當系統自帶的控制元件並不能滿足我們的需求時,可不可以利用上面的繼承結構來建立自定義控制元件呢?

答案是肯定的,下面我們就來學習一下建立自定義控制元件的兩種簡單方法。先將準備工作做好,建立一個UICustomViews專案。

3.4.1 引入佈局

如果你用過iPhone應該會知道,幾乎每一個iPhone應用的介面頂部都會有一個標題欄,標題欄上會有一到兩個按鈕可用於返回或其他操作(iPhone沒有實體返回鍵)。

現在很多Android程式也都喜歡模仿iPhone的風格,在介面的頂部放置一個標題欄。雖然Android系統已經給每個活動提供了標題欄功能,但這裡我們決定先不使用它,而是建立一個自定義的標題欄。

一般我們的程式中可能有很多個活動都需要標題欄,如果在每個活動的佈局中都編寫一遍同樣的標題欄程式碼,明顯就會導致程式碼的大量重複。這個時候我們就可以使用引入佈局的方式來解決這個問題,新建一個佈局title.xml,程式碼如下所示:

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:background="@drawable/title_bg">
    <Button
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:id="@+id/title_back"
        android:layout_gravity="center"
        android:layout_margin="5dp"
        android:background="@drawable/back_bg"
        android:text="Back"
        android:textColor="#fff"/>
    <TextView
        android:layout_width="0dp"
        android:layout_height="wrap_content"
        android:id="@+id/title_text"
        android:layout_gravity="center"
        android:layout_weight="1"
        android:text="Title Text"
        android:textColor="#fff"
        android:textSize="24sp"/>
    <Button
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:id="@+id/title_edit"
        android:layout_gravity="center"
        android:layout_margin="5dp"
        android:background="@drawable/edit_bg"
        android:text="Edit"
        android:textColor="#fff"/>
</LinearLayout>

可以看到,在LinearLayout中分別加入了兩個Button和一個TextView,左邊的Button可用於返回,右邊的Button可用於編輯,中間的TextView則可以顯示一段標題文字。

android:background用於為佈局或控制元件指定一個背景,可以使用顏色或圖片來進行填充,這裡我提前準備好了3張圖片——title_bg.png、back_bg.png和edit_bg.png,分別用於作為標題欄、返回按鈕和編輯按鈕的背景。

另外,在兩個Button中我們都使用了android:layout_margin這個屬性,它可以指定控制元件在上下左右方向上偏移的距離,當然也可以使用android:layout_marginLeft或android:layout_marginTop等屬性來單獨指定控制元件在某個方向上偏移的距離。現在標題欄佈局已經編寫完成了,剩下的就是如何在程式中使用這個標題欄了,修改activity_main.xml中的程式碼,如下所示:

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent">
    <include layout="@layout/title"/>
</LinearLayout>

只需要通過一行include語句將標題欄佈局引入進來就可以了。最後別忘了在MainActivity中將系統自帶的標題欄隱藏掉,程式碼如下所示:

package com.zhouzhou.uicustomviews;

import androidx.appcompat.app.ActionBar;
import androidx.appcompat.app.AppCompatActivity;

import android.os.Bundle;

public class MainActivity extends AppCompatActivity {

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        ActionBar actionBar = getSupportActionBar();
        if (actionBar != null) {
            actionBar.hide();
        }
    }
}

這裡呼叫了getSupportActionBar()方法來獲得ActionBar的例項,然後再呼叫ActionBar的hide()方法將標題欄隱藏起來。效果如圖:

使用這種方式,不管有多少佈局需要新增標題欄,只需一行include語句就可以了。

3.4.2 建立自定義控制元件

引入佈局的技巧確實解決了重複編寫佈局程式碼的問題,但是如果佈局中有一些控制元件要求能夠響應事件,我們還是需要在每個活動中為這些控制元件單獨編寫一次事件註冊的程式碼。

比如說標題欄中的返回按鈕,其實不管是在哪一個活動中,這個按鈕的功能都是相同的,即銷燬當前活動。而如果在每一個活動中都需要重新註冊一遍返回按鈕的點選事件,無疑會增加很多重複程式碼,這種情況最好是使用自定義控制元件的方式來解決。

新建TitleLayout繼承自LinearLayout,讓它成為我們自定義的標題欄控制元件,程式碼如下所示:

package com.zhouzhou.uicustomviews;

import android.content.Context;
import android.util.AttributeSet;
import android.view.LayoutInflater;
import android.widget.LinearLayout;

import androidx.annotation.Nullable;

public class TitleLayout extends LinearLayout{

    public TitleLayout(Context context, @Nullable AttributeSet attrs) {
        super(context, attrs);
        LayoutInflater.from(context).inflate(R.layout.title,this);
    }
}

首先重寫了LinearLayout中帶有兩個引數的建構函式,在佈局中引入TitleLayout控制元件就會呼叫這個建構函式。然後在建構函式中需要對標題欄佈局進行動態載入,這就要藉助LayoutInflater來實現了。通過LayoutInflater的from()方法可以構建出一個LayoutInflater物件,然後呼叫inflate()方法就可以動態載入一個佈局檔案,inflate()方法接收兩個引數,第一個引數是要載入的佈局檔案的id,這裡我們傳入R.layout.title,第二個引數是給載入好的佈局再新增一個父佈局,這裡我們想要指定為TitleLayout,於是直接傳入this。

現在自定義控制元件已經建立好了,然後我們需要在佈局檔案中新增這個自定義控制元件,修改activity_main.xml中的程式碼,如下所示:

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent">
    <com.zhouzhou.uicustomviews.TitleLayout
        android:layout_width="match_parent"
        android:layout_height="wrap_content"/>
</LinearLayout>

新增自定義控制元件和新增普通控制元件的方式基本是一樣的,只不過在新增自定義控制元件的時候,我們需要指明控制元件的完整類名,包名在這裡是不可以省略的。

重新執行程式,發現此時效果和使用引入佈局方式的效果是一樣的。嘗試為標題欄中的按鈕註冊點選事件,修改TitleLayout中的程式碼,如下所示:

package com.zhouzhou.uicustomviews;

import android.app.Activity;
import android.content.Context;
import android.util.AttributeSet;
import android.view.LayoutInflater;
import android.view.View;
import android.widget.Button;
import android.widget.LinearLayout;
import android.widget.Toast;
import androidx.annotation.Nullable;

public class TitleLayout extends LinearLayout{

    public TitleLayout(Context context, @Nullable AttributeSet attrs) {
        super(context, attrs);
        LayoutInflater.from(context).inflate(R.layout.title,this);
        Button titleBack = (Button) findViewById(R.id.title_back);
        Button titleEdit = (Button) findViewById(R.id.title_edit);
        titleBack.setOnClickListener(new OnClickListener() {
            @Override
            public void onClick(View view) {
                ((Activity)getContext()).finish();
            }
        });
        titleEdit.setOnClickListener(new OnClickListener() {
            @Override
            public void onClick(View view) {
                Toast.makeText(getContext(),"You clicked Edit button", Toast.LENGTH_SHORT).show();
            }
        });
    }
}

首先還是通過findViewById()方法得到按鈕的例項,然後分別呼叫setOnClickListener()方法給兩個按鈕註冊了點選事件,當點選返回按鈕時銷燬掉當前的活動,當點選編輯按鈕時彈出一段文字。重新執行程式,點選一下編輯按鈕,效果如圖:

這樣,每當我們在一個佈局中引入TitleLayout時,返回按鈕和編輯按鈕的點選事件就已經自動實現好了,這就省去了很多編寫重複程式碼的工作。

3.5 最常用和最難用的控制元件——ListView

ListView絕對可以稱得上是Android中最常用的控制元件之一,幾乎所有的應用程式都會用到它。

由於手機螢幕空間都比較有限,能夠一次性在螢幕上顯示的內容並不多,當我們的程式中有大量的資料需要展示的時候,就可以藉助ListView來實現。

ListView允許使用者通過手指上下滑動的方式將螢幕外的資料滾動到螢幕內,同時螢幕上原有的資料則會滾動出螢幕。比如檢視QQ聊天記錄,翻閱微博最新訊息,等等。不過比起前面介紹的幾種控制元件,ListView的用法也相對複雜了很多。

3.5.1 ListView的簡單用法

首先新建一個ListViewTest專案,並讓Android Studio自動幫我們建立好活動。然後修改activity_main.xml中的程式碼,如下所示:

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent">
    <ListView
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:id="@+id/list_view"/>
</LinearLayout>

在佈局中加入ListView控制元件還算非常簡單,先為ListView指定一個id,然後將寬度和高度都設定為match_parent,這樣ListView也就佔滿了整個佈局的空間。接下來修改MainActivity中的程式碼,如下所示:

package com.zhouzhou.listviewtest;

import androidx.appcompat.app.AppCompatActivity;
import android.os.Bundle;
import android.widget.ArrayAdapter;
import android.widget.ListView;

public class MainActivity extends AppCompatActivity {
    private String[] data = {"Apple","Banana","Orange","Watermelon",
            "Pear","Grape","Pineapple","Strawberry", "Cherry","Mango",
        "Apple","Banana","Orange","Watermelon","Pear","Grape","Pineapple","Strawberry",
            "Cherry","Mango"};
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        ArrayAdapter<String> adapter = new ArrayAdapter<String>(
                MainActivity.this,android.R.layout.simple_list_item_1,data);
        ListView listView = (ListView)findViewById(R.id.list_view);
        listView.setAdapter(adapter);
    }
}

既然ListView是用於展示大量資料的,那我們就應該先將資料提供好。這裡簡單使用了一個data陣列來測試,裡面包含了很多水果的名稱。

不過,陣列中的資料是無法直接傳遞給ListView的,我們還需要藉助介面卡來完成

Android中提供了很多介面卡的實現類,其中我認為最好用的就是ArrayAdapter。它可以通過泛型來指定要適配的資料型別,然後在建構函式中把要適配的資料傳入。

ArrayAdapter有多個建構函式的過載,你應該根據實際情況選擇最合適的一種。這裡由於我們提供的資料都是字串,因此將ArrayAdapter的泛型指定為String,然後在ArrayAdapter的建構函式中依次傳入當前上下文、ListView子項佈局的id,以及要適配的資料。

注意,使用了android.R.layout.simple_list_item_1作為ListView子項佈局的id,這是一個Android內建的佈局檔案,裡面只有一個TextView,可用於簡單地顯示一段文字。這樣介面卡物件就構建好了。

最後,還需要呼叫ListView的setAdapter()方法,將構建好的介面卡物件傳遞進去,這樣ListView和資料之間的關聯就建立完成了。

現在執行一下程式,效果如圖。可以通過滾動的方式來檢視螢幕外的資料:

3.5.2 定製ListView的介面

只能顯示一段文字的ListView實在是太單調了,現在就來對ListView的介面進行定製,讓它可以顯示更加豐富的內容。

首先需要準備好一組圖片,分別對應上面提供的每一種水果,待會我們要讓這些水果名稱的旁邊都有一個圖樣。接著定義一個實體類,作為ListView介面卡的適配型別。新建類Fruit,程式碼如下所示:

package com.zhouzhou.listviewtest;

public class Fruit {
    private String name;
    private int imageId;

    //快捷鍵:Alt+Insert 或者 工具欄:Code->Generate
    public Fruit(String name, int imageId) {
        this.name = name;
        this.imageId = imageId;
    }
    public String getName() {
        return name;
    }
    public int getImageId() {
        return imageId;
    }
}

Fruit類中只有兩個欄位,name表示水果的名字,imageId表示水果對應圖片的資源id。然後需要為ListView的子項指定一個我們自定義的佈局,在layout目錄下新建fruit_item.xml,程式碼如下所示:

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="wrap_content">
    <ImageView
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:id="@+id/fruit_image"/>
    <TextView
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:id="@+id/fruit_name"
        android:layout_gravity="center_vertical"
        android:layout_marginLeft="10dp"/>
</LinearLayout>

在這個佈局中,定義了一個ImageView用於顯示水果的圖片,又定義了一個TextView用於顯示水果的名稱,並讓TextView在垂直方向上居中顯示。

接下來需要建立一個自定義的介面卡,這個介面卡繼承自ArrayAdapter,並將泛型指定為Fruit類。新建類FruitAdapter,程式碼如下所示:

package com.zhouzhou.listviewtest;

import android.content.Context;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.ArrayAdapter;
import android.widget.ImageView;
import android.widget.TextView;
import java.util.List;

public class FruitAdapter extends ArrayAdapter<Fruit> {
    private int resourceId;
    public FruitAdapter(Context context, int textViewResourceId, List<Fruit> objects) {
        super(context,textViewResourceId,objects);
        resourceId = textViewResourceId;
    }
    @Override
    public View getView(int position,View convertView,ViewGroup parent) {
        Fruit fruit = getItem(position);//獲取當前項的Fruit例項
        View view = LayoutInflater.from(getContext()).inflate(resourceId,parent,false);
        ImageView fruitImage = (ImageView) view.findViewById(R.id.fruit_image);
        TextView fruitName = (TextView) view.findViewById(R.id.fruit_name);
        fruitImage.setImageResource(fruit.getImageId());
        fruitName.setText(fruit.getName());
        return view;
    }
}
  • FruitAdapter重寫了父類的一組建構函式,用於將上下文、ListView子項佈局的id和資料都傳遞進來。
  • 重寫了getView()方法,這個方法在每個子項被滾動到螢幕內的時候會被呼叫。
  • 在getView()方法中,首先通過getItem()方法得到當前項的Fruit例項,然後使用LayoutInflater來為這個子項載入我們傳入的佈局。
  • LayoutInflater的inflate()方法接收3個引數,第三個引數指定成false,表示只讓在父佈局中宣告的layout屬性生效,但不會為這個View新增父佈局,因為一旦View有了父佈局之後,它就不能再新增到ListView中了。這是ListView中的標準寫法,當你以後對View理解得更加深刻的時候,再來讀這段話就沒有問題了。
  • 呼叫View的findViewById()方法分別獲取到ImageView和TextView的例項,並分別呼叫它們的setImageResource()和setText()方法來設定顯示的圖片和文字.
  • 最後將佈局返回,這樣我們自定義的介面卡就完成了。
  • 下面修改MainActivity中的程式碼,如下所示:
package com.zhouzhou.listviewtest;

import androidx.appcompat.app.AppCompatActivity;
import android.os.Bundle;
import android.widget.ListView;

import java.util.ArrayList;
import java.util.List;

public class MainActivity extends AppCompatActivity {
    private List<Fruit> fruitList = new ArrayList<>();
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        initFruits();//初始化水果資料
        FruitAdapter adapter = new FruitAdapter(MainActivity.this,R.layout.fruit_item,fruitList);
        ListView listView = (ListView) findViewById(R.id.list_view);
        listView.setAdapter(adapter);
    }
    private void initFruits() {
        for (int i = 0; i < 2; i++) {
            Fruit apple = new Fruit("Apple",R.drawable.apple_pic);
            fruitList.add(apple);
            Fruit banana = new Fruit("Banana",R.drawable.banana_pic);
            fruitList.add(banana);
            Fruit cherry = new Fruit("Cherry",R.drawable.cherry_pic);
            fruitList.add(cherry);
            Fruit grape = new Fruit("Grape",R.drawable.grape_pic);
            fruitList.add(grape);
            Fruit mango = new Fruit("Mango",R.drawable.mango_pic);
            fruitList.add(mango);
            Fruit orange = new Fruit("Orange",R.drawable.orange_pic);
            fruitList.add(orange);
            Fruit pear = new Fruit("Pear",R.drawable.pear_pic);
            fruitList.add(pear);
            Fruit watermelon = new Fruit("Watermelon",R.drawable.watermelon_pic);
            fruitList.add(watermelon);
            Fruit pineapple = new Fruit("Pineapple",R.drawable.pineapple_pic);
            fruitList.add(pineapple);
            Fruit strawberry = new Fruit("Strawberry",R.drawable.strawberry_pic);
            fruitList.add(strawberry);
        }
    }
}
  • 添加了一個initFruits()方法,用於初始化所有的水果資料。
  • 在Fruit類的建構函式中將水果的名字和對應的圖片id傳入,把建立好的物件新增到水果列表中。
  • 使用for迴圈將所有的水果資料添加了兩遍,只新增一遍,資料量還不足以充滿整個螢幕。
  • 接著在onCreate()方法中建立了FruitAdapter物件,並將FruitAdapter作為介面卡傳遞給ListView。

這樣定製ListView介面的任務就完成了。現在重新執行程式,效果如圖所示:

3.5.3 提升ListView的執行效率

ListView這個控制元件很難用,因為它有很多細節可以優化,其中執行效率就是很重要的一點。

目前我們ListView的執行效率是很低的,因為在FruitAdapter的getView()方法中,每次都將佈局重新載入了一遍,當ListView快速滾動的時候,這就會成為效能的瓶頸。

仔細觀察會發現,getView()方法中還有一個convertView引數,這個引數用於將之前載入好的佈局進行快取,以便之後可以進行重用。修改FruitAdapter中的程式碼,如下所示:

package com.zhouzhou.listviewtest;

import android.content.Context;
import android.media.Image;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.ArrayAdapter;
import android.widget.ImageView;
import android.widget.TextView;
import java.util.List;

public class FruitAdapter extends ArrayAdapter<Fruit> {
    private int resourceId;
    public FruitAdapter(Context context, int textViewResourceId, List<Fruit> objects) {
        super(context,textViewResourceId,objects);
        resourceId = textViewResourceId;
    }
    @Override
    public View getView(int position,View convertView,ViewGroup parent) {
        Fruit fruit = getItem(position);//獲取當前項的Fruit例項
        /**
         * View view = LayoutInflater.from(getContext()).inflate(resourceId,parent,false);
         * ImageView fruitImage = (ImageView) view.findViewById(R.id.fruit_image);
         * TextView fruitName = (TextView) view.findViewById(R.id.fruit_name);
         * fruitImage.setImageResource(fruit.getImageId());
         * fruitName.setText(fruit.getName());
         **/
        View view;
        if(convertView == null){
            view = LayoutInflater.from(getContext()).inflate(resourceId,parent,false);
        }else{
            view = convertView;
        }
        ImageView fruitImage = (ImageView) view.findViewById(R.id.fruit_image);
        TextView fruitName = (TextView) view.findViewById(R.id.fruit_name);
        fruitImage.setImageResource(fruit.getImageId());
        fruitName.setText(fruit.getName());
        return view;
    }
}

可以看到,在getView()方法中進行了判斷,如果convertView為null,則使用LayoutInflater去載入佈局,如果不為null則直接對convertView進行重用。這樣就大大提高了ListView的執行效率,在快速滾動的時候也可以表現出更好的效能。

不過,目前這份程式碼還是可以繼續優化的,雖然現在已經不會再重複去載入佈局,但是每次在getView()方法中還是會呼叫View的findViewById()方法來獲取一次控制元件的例項。可以藉助一個ViewHolder來對這部分效能進行優化,修改FruitAdapter中的程式碼,如下所示:

package com.zhouzhou.listviewtest;

import android.content.Context;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.ArrayAdapter;
import android.widget.ImageView;
import android.widget.TextView;

import java.util.List;

public class FruitAdapter extends ArrayAdapter<Fruit> {
    private int resourceId;
    public FruitAdapter(Context context, int textViewResourceId, List<Fruit> objects) {
        super(context,textViewResourceId,objects);
        resourceId = textViewResourceId;
    }
    @Override
    public View getView(int position,View convertView,ViewGroup parent) {

        /**
         * View view = LayoutInflater.from(getContext()).inflate(resourceId,parent,false);
         * ImageView fruitImage = (ImageView) view.findViewById(R.id.fruit_image);
         * TextView fruitName = (TextView) view.findViewById(R.id.fruit_name);
         * fruitImage.setImageResource(fruit.getImageId());
         * fruitName.setText(fruit.getName());
         **/
        Fruit fruit = getItem(position);//獲取當前項的Fruit例項
        View view;
        ViewHolder viewHolder;
        if(convertView == null){
            view = LayoutInflater.from(getContext()).inflate(resourceId,parent,false);
            viewHolder = new ViewHolder();
            viewHolder.fruitImage = (ImageView) view.findViewById(R.id.fruit_image);
            viewHolder.fruitName = (TextView) view.findViewById(R.id.fruit_name);
            view.setTag(viewHolder);//將ViewHolder儲存在View中
        }else{
            view = convertView;
            viewHolder = (ViewHolder) view.getTag();//重新獲取viewHolder
        }
        /**
         * ImageView fruitImage = (ImageView) view.findViewById(R.id.fruit_image);
         * TextView fruitName = (TextView) view.findViewById(R.id.fruit_name);
         * fruitImage.setImageResource(fruit.getImageId());
         * fruitName.setText(fruit.getName());
         **/
        viewHolder.fruitImage.setImageResource(fruit.getImageId());
        viewHolder.fruitName.setText(fruit.getName());
        return view;
    }
        class ViewHolder{
        ImageView fruitImage;
        TextView fruitName;
    }
}

新增了一個內部類ViewHolder,用於對控制元件的例項進行快取。

  • 當convertView為null的時候,建立一個ViewHolder物件,並將控制元件的例項都存放在ViewHolder裡,然後呼叫View的setTag()方法,將ViewHolder物件儲存在View中。
  • 當convertView不為null的時候,則呼叫View的getTag()方法,把ViewHolder重新取出。

這樣所有控制元件的例項都快取在了ViewHolder裡,就沒有必要每次都通過findViewById()方法來獲取控制元件例項了。通過這兩步優化之後,我們ListView的執行效率就已經非常不錯了。

3.5.4 ListView的點選事件

ListView的滾動畢竟只是滿足了視覺上的效果,可是如果ListView中的子項不能點選的話,這個控制元件就沒有什麼實際的用途了。ListView如何才能響應使用者的點選事件。修改MainActivity中的程式碼,如下所示:

package com.zhouzhou.listviewtest;

import androidx.appcompat.app.AppCompatActivity;
import android.os.Bundle;
import android.view.View;
import android.widget.AdapterView;
import android.widget.ListView;
import android.widget.Toast;

import java.util.ArrayList;
import java.util.List;

public class MainActivity extends AppCompatActivity {
    private List<Fruit> fruitList = new ArrayList<>();
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        initFruits();//初始化水果資料
        FruitAdapter adapter = new FruitAdapter(MainActivity.this,R.layout.fruit_item,fruitList);
        ListView listView = (ListView) findViewById(R.id.list_view);
        listView.setAdapter(adapter);
        listView.setOnItemClickListener(new AdapterView.OnItemClickListener() {
            @Override
            public void onItemClick(AdapterView<?> adapterView, View view, int position, long id) {
                Fruit fruit = fruitList.get(position);
                Toast.makeText(MainActivity.this,fruit.getName(),Toast.LENGTH_SHORT).show();
            }
        });
    }
    private void initFruits() {
        for (int i = 0; i < 2; i++) {
            Fruit apple = new Fruit("Apple",R.drawable.apple_pic);
            fruitList.add(apple);
            Fruit banana = new Fruit("Banana",R.drawable.banana_pic);
            fruitList.add(banana);
            Fruit cherry = new Fruit("Cherry",R.drawable.cherry_pic);
            fruitList.add(cherry);
            Fruit grape = new Fruit("Grape",R.drawable.grape_pic);
            fruitList.add(grape);
            Fruit mango = new Fruit("Mango",R.drawable.mango_pic);
            fruitList.add(mango);
            Fruit orange = new Fruit("Orange",R.drawable.orange_pic);
            fruitList.add(orange);
            Fruit pear = new Fruit("Pear",R.drawable.pear_pic);
            fruitList.add(pear);
            Fruit watermelon = new Fruit("Watermelon",R.drawable.watermelon_pic);
            fruitList.add(watermelon);
            Fruit pineapple = new Fruit("Pineapple",R.drawable.pineapple_pic);
            fruitList.add(pineapple);
            Fruit strawberry = new Fruit("Strawberry",R.drawable.strawberry_pic);
            fruitList.add(strawberry);
        }
    }
}

使用setOnItemClickListener()方法為ListView註冊了一個監聽器,當用戶點選了ListView中的任何一個子項時,就會回撥onItemClick()方法。在這個方法中可以通過position引數判斷出使用者點選的是哪一個子項,然後獲取到相應的水果,並通過Toast將水果的名字顯示出來。重新執行程式,並點選一下橘子,效果如圖:

3.6 更強大的滾動控制元件——RecyclerView

ListView由於其強大的功能,在過去的Android開發當中可以說是貢獻卓越,直到今天仍然還有不計其數的程式在繼續使用著ListView。

不過ListView並不是完全沒有缺點的,比如說如果我們不使用一些技巧來提升它的執行效率,那麼ListView的效能就會非常差。還有,ListView的擴充套件性也不夠好,它只能實現資料縱向滾動的效果,如果我們想實現橫向滾動的話,ListView是做不到的。

為此,Android提供了一個更強大的滾動控制元件——RecyclerView。它可以說是一個增強版的ListView,不僅可以輕鬆實現和ListView同樣的效果,還優化了ListView中存在的各種不足之處。

目前Android官方更加推薦使用RecyclerView,未來也會有更多的程式逐漸從ListView轉向RecyclerView。首先新建一個RecyclerViewTest專案,並讓Android Studio自動幫我們建立好活動。

3.6.1 RecyclerView的基本用法

和百分比佈局類似,RecyclerView也屬於新增的控制元件,為了讓RecyclerView在所有Android版本上都能使用,Android團隊採取了同樣的方式,將RecyclerView定義在了support庫當中。

想要使用RecyclerView這個控制元件:1.首先需要在專案的build.gradle中新增相應的依賴庫才行(開啟app/build.gradle檔案,在dependencies閉包中新增依賴庫,書中是舊的版本,不能用了。在build.gradle中新增依賴庫的方式比較麻煩)。

  1. 首先,在activity_main.xml的Design模式下,選擇RecyclerView ——>右擊Add to Design
  1. 編輯activity_main.xml程式碼:
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent">

    <androidx.recyclerview.widget.RecyclerView
        android:id="@+id/recycler_view"
        android:layout_width="match_parent"
        android:layout_height="match_parent" />
</LinearLayout>
  • 佈局中加入了RecyclerView控制元件,先為RecyclerView指定一個id,然後將寬度和高度都設定為match_parent,這樣RecyclerView也就佔滿了整個佈局的空間。(需要注意的是,由於RecyclerView並不是內建在系統SDK當中的,所以需要把完整的包路徑寫出來。)
  • 這裡想要使用RecyclerView來實現和ListView相同的效果,因此就需要準備一份同樣的水果圖片。
  • 簡單起見,直接從ListViewTest專案中把圖片複製過來,另外將Fruit類和fruit_item.xml也複製過來。
  • 需要為RecyclerView準備一個介面卡,新建FruitAdapter類,讓這個介面卡繼承自RecyclerView.Adapter,並將泛型指定為FruitAdapter.ViewHolder。其中,ViewHolder是我們在FruitAdapter中定義的一個內部類,程式碼如下所示:
package com.zhouzhou.recyclerviewtest;

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.recyclerview.widget.RecyclerView;

import java.util.List;

public class FruitAdapter extends RecyclerView.Adapter<FruitAdapter.ViewHolder> {
    private List<Fruit> mFruitList;
    static class ViewHolder extends RecyclerView.ViewHolder{
        ImageView fruitImage;
        TextView fruitName;
        public ViewHolder(View view){
            super(view);
            fruitImage = (ImageView) view.findViewById(R.id.fruit_image);
            fruitName = (TextView) view.findViewById(R.id.fruit_name);
        }
    }
    public FruitAdapter(List<Fruit> fruitList){
        mFruitList = fruitList;
    }
    @NonNull
    @Override
    public ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
        View view = LayoutInflater.from(parent.getContext()).inflate(R.layout.fruit_item,parent,false);
        ViewHolder holder = new ViewHolder(view);
        return holder;
    }
    @Override
    public void onBindViewHolder(@NonNull ViewHolder holder, int position) {
        Fruit fruit = mFruitList.get(position);
        holder.fruitImage.setImageResource(fruit.getImageId());
        holder.fruitName.setText(fruit.getName());
    }
    @Override
    public int getItemCount() {
        return mFruitList.size();
    }
}

這段程式碼其實比ListView的介面卡要更容易理解。

  • 首先定義了一個內部類ViewHolder, ViewHolder要繼承自RecyclerView.ViewHolder。
  • 然後ViewHolder的建構函式中要傳入一個View引數,這個引數通常就是RecyclerView子項的最外層佈局,那麼我們就可以通過findViewById()方法來獲取到佈局中的ImageView和TextView的例項了。
  • 接著,FruitAdapter中也有一個建構函式,這個方法用於把要展示的資料來源傳進來,並賦值給一個全域性變數mFruitList,後續的操作都將在這個資料來源的基礎上進行。

由於FruitAdapter是繼承自RecyclerView.Adapter的,那麼就必須重寫onCreateViewHolder()、onBindViewHolder()和getItemCount()這3個方法。

  • onCreateViewHolder()方法是用於建立ViewHolder例項的,在這個方法中將fruit_item佈局載入進來,然後建立一個ViewHolder例項,並把加載出來的佈局傳入到建構函式當中,最後將ViewHolder的例項返回。
  • onBindViewHolder()方法是用於對RecyclerView子項的資料進行賦值的,會在每個子項被滾動到螢幕內的時候執行,這裡我們通過position引數得到當前項的Fruit例項,然後再將資料設定到ViewHolder的ImageView和TextView當中即可。
  • getItemCount()方法就非常簡單了,它用於告訴RecyclerView一共有多少子項,直接返回資料來源的長度就可以了。

介面卡準備好了之後,我們就可以開始使用RecyclerView了,修改MainActivity中的程式碼,如下所示:

package com.zhouzhou.recyclerviewtest;

import androidx.appcompat.app.AppCompatActivity;
import androidx.fragment.app.FragmentTransitionImpl;
import androidx.recyclerview.widget.LinearLayoutManager;
import androidx.recyclerview.widget.RecyclerView;

import android.os.Bundle;

import java.util.ArrayList;
import java.util.List;

public class MainActivity extends AppCompatActivity {
    private List<Fruit> fruitList = new ArrayList<>();

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        initFruits();//初始化水果資料
        RecyclerView recyclerView = (RecyclerView) findViewById(R.id.recycler_view);
        LinearLayoutManager layoutManager = new LinearLayoutManager(this);
        recyclerView.setLayoutManager(layoutManager);
        FruitAdapter adapter = new FruitAdapter(fruitList);
        recyclerView.setAdapter(adapter);
    }

    private void initFruits() {
        for (int i = 0; i < 2; i++) {
            Fruit apple = new Fruit("Apple", R.drawable.apple_pic);
            fruitList.add(apple);
            Fruit banana = new Fruit("Banana", R.drawable.banana_pic);
            fruitList.add(banana);
            Fruit cherry = new Fruit("Cherry", R.drawable.cherry_pic);
            fruitList.add(cherry);
            Fruit grape = new Fruit("Grape", R.drawable.grape_pic);
            fruitList.add(grape);
            Fruit mango = new Fruit("Mango", R.drawable.mango_pic);
            fruitList.add(mango);
            Fruit orange = new Fruit("Orange", R.drawable.orange_pic);
            fruitList.add(orange);
            Fruit pear = new Fruit("Pear", R.drawable.pear_pic);
            fruitList.add(pear);
            Fruit watermelon = new Fruit("Watermelon", R.drawable.watermelon_pic);
            fruitList.add(watermelon);
            Fruit pineapple = new Fruit("Pineapple", R.drawable.pineapple_pic);
            fruitList.add(pineapple);
            Fruit strawberry = new Fruit("Strawberry", R.drawable.strawberry_pic);
            fruitList.add(strawberry);
        }
    }
}

可以看到,這裡使用了一個同樣的initFruits()方法,用於初始化所有的水果資料。接著在onCreate()方法中先獲取到RecyclerView的例項,然後建立了一個LinearLayout-Manager物件,並將它設定到RecyclerView當中。LayoutManager用於指定RecyclerView的佈局方式,這裡使用的LinearLayoutManager是線性佈局的意思,可以實現和ListView類似的效果。

接下來我們建立了FruitAdapter的例項,並將水果資料傳入到FruitAdapter的建構函式中,最後呼叫RecyclerView的setAdapter()方法來完成介面卡設定,這樣RecyclerView和資料之間的關聯就建立完成了。現在可以執行一下程式了,效果如圖:

3.6.2 實現橫向滾動和瀑布流佈局

ListView的擴充套件性並不好,它只能實現縱向滾動的效果,如果想進行橫向滾動的話,ListView就做不到了。那麼RecyclerView就能做得到嗎?當然可以,不僅能做得到,還非常簡單,那麼接下來我們就嘗試實現一下橫向滾動的效果。

首先要對fruit_item佈局進行修改,因為目前這個佈局裡面的元素是水平排列的,適用於縱向滾動的場景,而如果我們要實現橫向滾動的話,應該把fruit_item裡的元素改成垂直排列才比較合理。修改fruit_item.xml中的程式碼,如下所示:

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:orientation="vertical"
    android:layout_width="100dp"
    android:layout_height="wrap_content">
    <ImageView
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:id="@+id/fruit_image"
        android:layout_gravity="center_horizontal"/>
    <TextView
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:id="@+id/fruit_name"
        android:layout_gravity="center_horizontal"
        android:layout_marginLeft="10dp"/>
</LinearLayout>

將LinearLayout改成垂直方向排列,並把寬度設為100dp。這裡將寬度指定為固定值是因為每種水果的文字長度不一致,如果用wrap_content的話,RecyclerView的子項就會有長有短,非常不美觀;而如果用match_parent的話,就會導致寬度過長,一個子項佔滿整個螢幕。

然後將ImageView和TextView都設定成了在佈局中水平居中,並且使用layout_marginTop屬性讓文字和圖片之間保持一些距離。接下來修改MainActivity中的程式碼,如下所示:

package com.zhouzhou.recyclerviewtest;

import androidx.appcompat.app.AppCompatActivity;
import androidx.fragment.app.FragmentTransitionImpl;
import androidx.recyclerview.widget.LinearLayoutManager;
import androidx.recyclerview.widget.RecyclerView;

import android.os.Bundle;

import java.util.ArrayList;
import java.util.List;

public class MainActivity extends AppCompatActivity {
    private List<Fruit> fruitList = new ArrayList<>();
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        initFruits();//初始化水果資料
        RecyclerView recyclerView = (RecyclerView) findViewById(R.id.recycler_view);
        LinearLayoutManager layoutManager = new LinearLayoutManager(this);
        //呼叫LinearLayoutManager的setOrientation()方法來設定佈局的排列方向,預設是縱向排列的
        layoutManager.setOrientation(LinearLayoutManager.HORIZONTAL);
        recyclerView.setLayoutManager(layoutManager);
        FruitAdapter adapter = new FruitAdapter(fruitList);
        recyclerView.setAdapter(adapter);
    }

    private void initFruits() {
        for (int i = 0; i < 2; i++) {
            Fruit apple = new Fruit("Apple", R.drawable.apple_pic);
            fruitList.add(apple);
            Fruit banana = new Fruit("Banana", R.drawable.banana_pic);
            fruitList.add(banana);
            Fruit cherry = new Fruit("Cherry", R.drawable.cherry_pic);
            fruitList.add(cherry);
            Fruit grape = new Fruit("Grape", R.drawable.grape_pic);
            fruitList.add(grape);
            Fruit mango = new Fruit("Mango", R.drawable.mango_pic);
            fruitList.add(mango);
            Fruit orange = new Fruit("Orange", R.drawable.orange_pic);
            fruitList.add(orange);
            Fruit pear = new Fruit("Pear", R.drawable.pear_pic);
            fruitList.add(pear);
            Fruit watermelon = new Fruit("Watermelon", R.drawable.watermelon_pic);
            fruitList.add(watermelon);
            Fruit pineapple = new Fruit("Pineapple", R.drawable.pineapple_pic);
            fruitList.add(pineapple);
            Fruit strawberry = new Fruit("Strawberry", R.drawable.strawberry_pic);
            fruitList.add(strawberry);
        }
    }
}

MainActivity中只加入了一行程式碼,呼叫LinearLayoutManager的setOrientation()方法來設定佈局的排列方向,預設是縱向排列的,傳入LinearLayoutManager.HORIZONTAL表示讓佈局橫行排列,這樣RecyclerView就可以橫向滾動了。重新執行一下程式,效果如圖:

可以用手指在水平方向上滑動來檢視螢幕外的資料。

為什麼ListView很難或者根本無法實現的效果在RecyclerView上這麼輕鬆就能實現了呢?

這主要得益於RecyclerView出色的設計。ListView的佈局排列是由自身去管理的,而RecyclerView則將這個工作交給了LayoutManager,LayoutManager中制定了一套可擴充套件的佈局排列介面,子類只要按照介面的規範來實現,就能定製出各種不同排列方式的佈局了。

除了LinearLayoutManager之外,RecyclerView還給我們提供了GridLayoutManager和StaggeredGridLayoutManager這兩種內建的佈局排列方式。GridLayoutManager可以用於實現網格佈局,StaggeredGridLayoutManager可以用於實現瀑布流佈局。

這裡我們來實現一下效果更加炫酷的瀑布流佈局。首先還是來修改一下fruit_item.xml中的程式碼,如下所示:

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:orientation="vertical"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:layout_margin="5dp">
    <ImageView
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:id="@+id/fruit_image"
        android:layout_gravity="center_horizontal"/>
    <TextView
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:id="@+id/fruit_name"
        android:layout_gravity="left"
        android:layout_marginLeft="10dp"/>
</LinearLayout>

這裡做了幾處小的調整,首先將LinearLayout的寬度由100dp改成了match_parent,因為瀑布流佈局的寬度應該是根據佈局的列數來自動適配的,而不是一個固定值。

另外使用了layout_margin屬性來讓子項之間互留一點間距,這樣就不至於所有子項都緊貼在一些。還有就是將TextView的對齊屬性改成了居左對齊,因為待會我們會將文字的長度變長,如果還是居中顯示就會感覺怪怪的。接著修改MainActivity中的程式碼,如下所示:

package com.zhouzhou.recyclerviewtest;

import androidx.appcompat.app.AppCompatActivity;
import androidx.fragment.app.FragmentTransitionImpl;
import androidx.recyclerview.widget.LinearLayoutManager;
import androidx.recyclerview.widget.RecyclerView;
import androidx.recyclerview.widget.StaggeredGridLayoutManager;

import android.os.Bundle;

import java.util.ArrayList;
import java.util.List;
import java.util.Random;

public class MainActivity extends AppCompatActivity {
    private List<Fruit> fruitList = new ArrayList<>();
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        initFruits();//初始化水果資料
        RecyclerView recyclerView = (RecyclerView) findViewById(R.id.recycler_view);
        StaggeredGridLayoutManager layoutManager = new StaggeredGridLayoutManager(3,StaggeredGridLayoutManager.VERTICAL);
        //LinearLayoutManager layoutManager = new LinearLayoutManager(this);
        //呼叫LinearLayoutManager的setOrientation()方法來設定佈局的排列方向,預設是縱向排列的
        //layoutManager.setOrientation(LinearLayoutManager.HORIZONTAL);
        recyclerView.setLayoutManager(layoutManager);
        FruitAdapter adapter = new FruitAdapter(fruitList);
        recyclerView.setAdapter(adapter);
    }

    private void initFruits() {
        for (int i = 0; i < 2; i++) {
            Fruit apple = new Fruit(getRandomLengthName("Apple"), R.drawable.apple_pic);
            fruitList.add(apple);
            Fruit banana = new Fruit(getRandomLengthName("Banana"), R.drawable.banana_pic);
            fruitList.add(banana);
            Fruit cherry = new Fruit(getRandomLengthName("Cherry"), R.drawable.cherry_pic);
            fruitList.add(cherry);
            Fruit grape = new Fruit(getRandomLengthName("Grape"), R.drawable.grape_pic);
            fruitList.add(grape);
            Fruit mango = new Fruit(getRandomLengthName("Mango"), R.drawable.mango_pic);
            fruitList.add(mango);
            Fruit orange = new Fruit(getRandomLengthName("Orange"), R.drawable.orange_pic);
            fruitList.add(orange);
            Fruit pear = new Fruit(getRandomLengthName("Pear"), R.drawable.pear_pic);
            fruitList.add(pear);
            Fruit watermelon = new Fruit(getRandomLengthName("Watermelon"), R.drawable.watermelon_pic);
            fruitList.add(watermelon);
            Fruit pineapple = new Fruit(getRandomLengthName("Pineapple"), R.drawable.pineapple_pic);
            fruitList.add(pineapple);
            Fruit strawberry = new Fruit(getRandomLengthName("Strawberry"), R.drawable.strawberry_pic);
            fruitList.add(strawberry);
        }
    }
    private String getRandomLengthName(String name){
        Random random = new Random();
        int length = random.nextInt(20) + 1;
        StringBuilder builder = new StringBuilder();
        for (int i = 0; i < length; i++) {
            builder.append(name);
        }
        return builder.toString();
    }
}

首先,在onCreate()方法中,我們建立了一個StaggeredGridLayoutManager的例項。StaggeredGridLayoutManager的建構函式接收兩個引數,第一個引數用於指定佈局的列數,傳入3表示會把佈局分為3列;第二個引數用於指定佈局的排列方向,傳入StaggeredGrid-LayoutManager.VERTICAL表示會讓佈局縱向排列,最後再把建立好的例項設定到RecyclerView當中就可以了,就是這麼簡單!僅僅修改了一行程式碼,我們就已經成功實現瀑布流佈局的效果了。

不過由於瀑布流佈局需要各個子項的高度不一致才能看出明顯的效果,為此我又使用了一個小技巧。這裡我們把眼光聚焦在getRandomLengthName()這個方法上,這個方法使用了Random物件來創造一個1到20之間的隨機數,然後將引數中傳入的字串隨機重複幾遍。在initFruits()方法中,每個水果的名字都改成呼叫getRandomLengthName()這個方法來生成,這樣就能保證各水果名字的長短差距都比較大,子項的高度也就各不相同了。現在重新執行一下程式,效果如圖:

當然由於水果名字的長度每次都是隨機生成的,你執行時的效果肯定和圖中還是不一樣的。

3.6.3 RecyclerView的點選事件

和ListView一樣,RecyclerView也必須要能響應點選事件才可以,不然的話就沒什麼實際用途了。不過不同於ListView的是,RecyclerView並沒有提供類似於setOnItemClickListener()這樣的註冊監聽器方法,而是需要我們自己給子項具體的View去註冊點選事件,相比於ListView來說,實現起來要複雜一些。

為什麼RecyclerView在各方面的設計都要優於ListView,偏偏在點選事件上卻沒有處理得非常好呢?

其實不是這樣的,ListView在點選事件上的處理並不人性化,setOnItemClickListener()方法註冊的是子項的點選事件,但如果我想點選的是子項裡具體的某一個按鈕呢?雖然ListView也是能做到的,但是實現起來就相對比較麻煩了。為此,RecyclerView乾脆直接摒棄了子項點選事件的監聽器,所有的點選事件都由具體的View去註冊,就再沒有這個困擾了。

下面我們來具體學習一下如何在RecyclerView中註冊點選事件,修改FruitAdapter中的程式碼,如下所示:

package com.zhouzhou.recyclerviewtest;

import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.ImageView;
import android.widget.TextView;
import android.widget.Toast;

import androidx.annotation.NonNull;
import androidx.recyclerview.widget.RecyclerView;

import java.util.List;

public class FruitAdapter extends RecyclerView.Adapter<FruitAdapter.ViewHolder> {
    private List<Fruit> mFruitList;
    static class ViewHolder extends RecyclerView.ViewHolder{
        View fruitView;
        ImageView fruitImage;
        TextView fruitName;
        public ViewHolder(View view){
            super(view);
            fruitView = view;
            fruitImage = (ImageView) view.findViewById(R.id.fruit_image);
            fruitName = (TextView) view.findViewById(R.id.fruit_name);
        }
    }
    public FruitAdapter(List<Fruit> fruitList){
        mFruitList = fruitList;
    }
    @NonNull
    @Override
    public ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
        View view = LayoutInflater.from(parent.getContext()).inflate(R.layout.fruit_item,parent,false);
        final  ViewHolder holder = new ViewHolder(view);
        holder.fruitView.setOnClickListener(new View.OnClickListener() {
                                                @Override
                                                public void onClick(View view) {
                                                    int position = holder.getAdapterPosition();
                                                    Fruit fruit = mFruitList.get(position);
                                                    Toast.makeText(view.getContext(),"You clicked view " + fruit.getName(),Toast.LENGTH_SHORT).show();
                                                }
                                            });
        holder.fruitImage.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View view) {
                int position = holder.getAdapterPosition();
                Fruit fruit = mFruitList.get(position);
                Toast.makeText(view.getContext(),"You clicked image " + fruit.getName(),Toast.LENGTH_SHORT).show();
            }
        });
        return holder;
    }
    @Override
    public void onBindViewHolder(@NonNull ViewHolder holder, int position) {
        Fruit fruit = mFruitList.get(position);
        holder.fruitImage.setImageResource(fruit.getImageId());
        holder.fruitName.setText(fruit.getName());
    }
    @Override
    public int getItemCount() {
        return mFruitList.size();
    }
}

先是修改了ViewHolder,在ViewHolder中添加了fruitView變數來儲存子項最外層佈局的例項,然後在onCreateViewHolder()方法中註冊點選事件就可以了。這裡分別為最外層佈局和ImageView都註冊了點選事件,RecyclerView的強大之處也在這裡,它可以輕鬆實現子項中任意控制元件或佈局的點選事件

我們在兩個點選事件中先獲取了使用者點選的position,然後通過position拿到相應的Fruit例項,再使用Toast分別彈出兩種不同的內容以示區別。現在重新執行程式碼,並點選香蕉的圖片部分,效果如圖。可以看到,這時觸發了ImageView的點選事件。

再點選菠蘿的文字部分,由於TextView並沒有註冊點選事件,因此點選文字這個事件會被子項的最外層佈局捕獲到,效果如圖:

3.7 編寫介面的最佳實踐

這次要綜合運用前面所學的大量內容來編寫出一個較為複雜且相當美觀的聊天介面。要先建立一個UIBestPractice專案。

3.7.1 製作Nine-Patch圖片

在實戰正式開始之前,還需要先學習一下如何製作Nine-Patch圖片。它是一種被特殊處理過的png圖片,能夠指定哪些區域可以被拉伸、哪些區域不可以。Nine-Patch圖片到底有什麼實際作用呢?通過一個例子來看一下吧。比如說專案中有一張氣泡樣式的圖片message_left.png,如圖所示:

將這張圖片設定為LinearLayout的背景圖片,修改activity_main.xml中的程式碼,如下所示:

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:background="@drawable/message_left">
</LinearLayout>

將LinearLayout的寬度指定為match_parent,將它的背景圖設定為message_left,現在執行程式,效果如圖:

可以看到,由於message_left的寬度不足以填滿整個螢幕的寬度,整張圖片被均勻地拉伸了!

這種效果非常差,使用者肯定是不能容忍的,這時我們就可以使用Nine-Patch圖片來進行改善,9-patch是一種縮放不失真或變形的圖片格式,常用於聊天框的實現。

(書中所寫:“在Android sdk目錄下有一個tools資料夾,在這個資料夾中找到draw9patch.bat檔案,我們就是使用它來製作Nine-Patch圖片的。不過,要開啟這個檔案,必須先將JDK的bin目錄配置到環境變數當中才行,比如你使用的是Android Studio內建的jdk,那麼要配置的路徑就是<Android Studio安裝目錄>/jre/bin。如果你還不知道該如何配置環境變數,可以先去參考6.4.1小節的內容。雙擊開啟draw9patch.bat檔案,在導航欄點選File→Open 9-patch將message_left.png載入進來。”)

找不到draw9patch.bat?已經不用找了。Google 已經因為 draw9patch 熱門的原因,把它整合在 Android Studio 裡面了,你現在可以直接在 Android Studio 裡直接開啟編輯了。如圖所示:

點選 Create9-Patch file...後,message_left.9.png被載入進來了:

可以在圖片的四個邊框繪製一個個的小黑點,在上邊框和左邊框繪製的部分表示當圖片需要拉伸時就拉伸黑點標記的區域,在下邊框和右邊框繪製的部分表示內容會被放置的區域。使用滑鼠在圖片的邊緣拖動就可以進行繪製了,按住Shift鍵拖動可以進行擦除。繪製完成後效果如圖:

(書中所寫:“最後點選導航欄File→Save 9-patch把繪製好的圖片進行儲存,此時的檔名就是message_left.9.png。使用這張圖片替換掉之前的message_left.png圖片”,我執行之後,出現報錯Error:Duplicate resources 解決是,直接刪除了圖片message_left.png,並且在activity_main.xml檔案中,ndroid:background="@drawable/message_left"沒有更改)重新執行程式,效果如圖:

這樣當圖片需要拉伸的時候,就可以只拉伸指定的區域,程式在外觀上也有了很大的改進。

3.7.2 編寫精美的聊天介面

編寫一個聊天介面,那就肯定要有收到的訊息和發出的訊息。上一節中製作的message_left.9.png可以作為收到訊息的背景圖,還需要再製作一張message_right.9.png作為發出訊息的背景圖。圖片都提供好了之後就可以開始編碼了。(會用到RecyclerView)如下所示:

接下來開始編寫主介面,修改activity_main.xml中的程式碼,如下所示:

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:orientation="vertical"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:background="#d8e0e8">
    <androidx.recyclerview.widget.RecyclerView
        android:id="@+id/msg_recycler_view"
        android:layout_width="match_parent"
        android:layout_height="0dp"
        android:layout_weight="1"/>
    <LinearLayout
        android:layout_width="match_parent"
        android:layout_height="wrap_content">
        <EditText
            android:id="@+id/input_text"
            android:layout_width="0dp"
            android:layout_height="wrap_content"
            android:layout_weight="1"
            android:hint="Type something here"
            android:maxLines="2" />
        <Button
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:id="@+id/send"
            android:text="Send"/>
    </LinearLayout>   
</LinearLayout>

在主介面中放置了一個RecyclerView用於顯示聊天的訊息內容,又放置了一個EditText用於輸入訊息,還放置了一個Button用於傳送訊息。然後定義訊息的實體類,新建Msg,程式碼如下所示:

package com.zhouzhou.uibestpractice;

public class Msg {
    public static final int TYPE_RECEIVED = 0;
    public static final int TYPE_SENT = 1;
    private String content;
    private int type;

    public Msg(String content, int type) {
        this.content = content;
        this.type = type;
    }

    public String getContent() {
        return content;
    }

    public int getType() {
        return type;
    }
}

Msg類中只有兩個欄位,content表示訊息的內容,type表示訊息的型別。其中訊息型別有兩個值可選,TYPE_RECEIVED表示這是一條收到的訊息,TYPE_SENT表示這是一條發出的訊息。

接著來編寫RecyclerView子項的佈局,新建msg_item.xml,程式碼如下所示:

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:orientation="vertical"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:padding="10dp">
    <LinearLayout
        android:id="@+id/left_layout"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_gravity="left"
        android:background="@drawable/message_left">
        <TextView
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:id="@+id/left_msg"
            android:layout_gravity="center"
            android:layout_margin="10dp"
            android:textColor="#fff"/>
    </LinearLayout>
    <LinearLayout
        android:id="@+id/right_layout"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_gravity="right"
        android:background="@drawable/message_right">
        <TextView
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:id="@+id/right_msg"
            android:layout_gravity="center"
            android:layout_margin="10dp"/>
    </LinearLayout>
</LinearLayout>

這裡讓收到的訊息居左對齊,發出的訊息居右對齊,並且分別使用message_left.9.png和message_right.9.png作為背景圖。

你可能會有些疑慮,怎麼能讓收到的訊息和發出的訊息都放在同一個佈局裡呢?不用擔心,還記得我們前面學過的可見屬性嗎?只要稍後在程式碼中根據訊息的型別來決定隱藏和顯示哪種訊息就可以了。

接下來需要建立RecyclerView的介面卡類,新建類MsgAdapter,程式碼如下所示:

package com.zhouzhou.uibestpractice;

import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.LinearLayout;
import android.widget.TextView;
import androidx.annotation.NonNull;
import androidx.recyclerview.widget.RecyclerView;

import java.util.List;

public class MsgAdapter extends RecyclerView.Adapter<MsgAdapter.ViewHolder> {
    public MsgAdapter(List<Msg> mMsgList) {
        this.mMsgList = mMsgList;
    }
    private List<Msg> mMsgList;
    static class ViewHolder extends RecyclerView.ViewHolder {
        LinearLayout leftLayout;
        LinearLayout rightLayout;
        TextView leftMsg;
        TextView rightMsg;
        public ViewHolder(@NonNull View itemView) {
            super(itemView);
            leftLayout = (LinearLayout) itemView.findViewById(R.id.left_layout);
            rightLayout = (LinearLayout) itemView.findViewById(R.id.right_layout);
            leftMsg = (TextView) itemView.findViewById(R.id.left_msg);
            rightMsg = (TextView) itemView.findViewById(R.id.right_msg);
        }
    }
    @NonNull
    @Override
    public ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
        View view = LayoutInflater.from(parent.getContext()).inflate(R.layout.msg_item,parent,false);
        return new ViewHolder(view);
    }
    @Override
    public void onBindViewHolder(@NonNull ViewHolder holder, int position) {
        Msg msg = mMsgList.get(position);
        if (msg.getType() == Msg.TYPE_RECEIVED){
            //如果是收到的訊息,則顯示左邊的訊息佈局,將右邊的訊息佈局隱藏
            holder.leftLayout.setVisibility(View.VISIBLE);
            holder.rightLayout.setVisibility(View.GONE);
            holder.leftMsg.setText(msg.getContent());
        }else if (msg.getType() == Msg.TYPE_SENT){
            //如果是發出的訊息,則顯示右邊的訊息佈局,將左邊的訊息佈局隱藏
            holder.rightLayout.setVisibility(View.VISIBLE);
            holder.leftLayout.setVisibility(View.GONE);
            holder.rightMsg.setText(msg.getContent());
        }
    }
    @Override
    public int getItemCount() {
        return mMsgList.size();
    }
}

以上程式碼在onBindViewHolder()方法中增加了對訊息型別的判斷。如果這條訊息是收到的,則顯示左邊的訊息佈局,如果這條訊息是發出的,則顯示右邊的訊息佈局。

最後修改MainActivity中的程式碼,來為RecyclerView初始化一些資料,並給傳送按鈕加入事件響應,程式碼如下所示:

package com.zhouzhou.uibestpractice;

import androidx.appcompat.app.AppCompatActivity;
import androidx.recyclerview.widget.LinearLayoutManager;
import androidx.recyclerview.widget.RecyclerView;
import android.os.Bundle;
import android.view.View;
import android.widget.Button;
import android.widget.EditText;
import java.util.ArrayList;
import java.util.List;

public class MainActivity extends AppCompatActivity {
    private List<Msg> msgList = new ArrayList<>();
    private EditText inputText;
    private Button send;
    private RecyclerView msgRecyclerView;
    private MsgAdapter adapter;
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        initMsgs();//初始化訊息資料
        inputText = (EditText) findViewById(R.id.input_text);
        send = (Button) findViewById(R.id.send);
        msgRecyclerView = (RecyclerView) findViewById(R.id.msg_recycler_view);
        LinearLayoutManager layoutManager = new LinearLayoutManager(this);
        msgRecyclerView.setLayoutManager(layoutManager);
        adapter = new MsgAdapter(msgList);
        msgRecyclerView.setAdapter(adapter);
        send.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View view) {
                String content = inputText.getText().toString();
                if(!"".equals(content)){
                    Msg msg =new Msg(content,Msg.TYPE_SENT);
                    msgList.add(msg);
                    adapter.notifyItemInserted(msgList.size() - 1);//當有新訊息時,重新整理RecyclerView中的顯示
                    msgRecyclerView.scrollToPosition(msgList.size() - 1);//將RecyclerView定位到最後一行
                    inputText.setText("");//清空輸入框中的內容
                }
            }
        });
        
    }

    private void initMsgs() {
        Msg msg1 = new Msg("Hello guy.",Msg.TYPE_RECEIVED);
        msgList.add(msg1);
        Msg msg2 = new Msg("Hello.Who is that?",Msg.TYPE_SENT);
        msgList.add(msg2);
        Msg msg3 = new Msg("This is zhouzhou.Nice talking to you",Msg.TYPE_RECEIVED);
        msgList.add(msg3);
        Msg msg4 = new Msg("Hi zhouzhou.My name is xiaoming.",Msg.TYPE_RECEIVED);
        msgList.add(msg4);
    }
}

在initMsgs()方法中先初始化了幾條資料用於在RecyclerView中顯示。

在傳送按鈕的點選事件裡獲取了EditText中的內容,如果內容不為空字串則創建出一個新的Msg物件,並把它新增到msgList列表中去。

又呼叫了介面卡的notifyItemInserted()方法,用於通知列表有新的資料插入,這樣新增的一條訊息才能夠在RecyclerView中顯示。

呼叫RecyclerView的scrollToPosition()方法將顯示的資料定位到最後一行,以保證一定可以看得到最後發出的一條訊息。

呼叫EditText的setText()方法將輸入的內容清空。

這樣所有的工作就都完成了,終於可以檢驗一下我們的成果了,執行程式之後你將會看到非常美觀的聊天介面,並且可以輸入和傳送訊息,如圖: