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

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

2022-05-11 08:31:50 移動端開發

本文和接下來的幾篇文章為閱讀郭霖先生所著《第一行代碼: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_
    android:layout_height="match_parent">

    <TextView
        android:id="@+id/text_view"
        android:layout_
        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中顯示的文本內容,現在運行程式,效果如圖:

image

由于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_
    android:layout_height="match_parent">

    <TextView
        android:id="@+id/text_view"
        android:layout_
        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,表示文字在垂直和水平方向都居中對齊,現在重新運行程式,效果如圖:

image

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

 <TextView
        android:id="@+id/text_view"
        android:layout_
        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_
        android:layout_height="wrap_content"
        android:id="@+id/button"
        android:text="Button"/>

image

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

<Button
        android:layout_
        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_
        android:layout_height="wrap_content"
        android:id="@+id/edit_text"/>

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

所以使用XML來撰寫界面其實一點都不難,完全可以不用借助任何可視化工具來實作,現在重新運行一下程式,EditText就已經在界面上顯示出來了,并且我們是可以在里面輸入內容的,如圖:

image

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

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

image

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

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

    <EditText
        android:layout_
        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;
        }
    }
}

image

3.2.4 ImageView

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

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

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

    <ImageView
        android:layout_
        android:layout_height="wrap_content"
        android:id="@+id/image_view"
        android:src="https://www.cnblogs.com/1693977889zz/p/@drawable/img_1"/>

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

image

可以在程式中通過代碼動態地更改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中顯示的圖片改變了,如圖:

image

3.2.5 ProgressBar

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

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

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

image

一個新的知識點: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_
        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作為更新后的進度,重新運行程式,點擊數次按鈕后,效果如圖:

image

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()方法將對話框顯示出來,

重新運行程式,點擊按鈕后,效果如圖所示:

image

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顯示出來,重新運行程式,點擊按鈕后,效果如圖:

image

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

3.3 詳解4種基本布局

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

布局是一種可用于放置很多控制元件的容器,它可以按照一定的規律調整內部控制元件的位置,從而撰寫出精美的界面,當然,布局的內部除了放置控制元件外,也可以放置布局,通過多層布局的嵌套,我們就能夠完成一些比較復雜的界面實作,圖:

image

下面詳細講解下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_
    android:layout_height="match_parent">
    <Button
        android:layout_
        android:layout_height="wrap_content"
        android:id="@+id/button1"
        android:text="button1"/>
    <Button
        android:layout_
        android:layout_height="wrap_content"
        android:id="@+id/button2"
        android:text="button2"/>
    <Button
        android:layout_
        android:layout_height="wrap_content"
        android:id="@+id/button3"
        android:text="button3"/>

</LinearLayout>

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

image

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

android:orientation="horizontal"

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

image

這里需要注意,如果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_
    android:layout_height="match_parent">
    <Button
        android:layout_
        android:layout_height="wrap_content"
        android:id="@+id/button1"
        android:text="button1"
        android:layout_gravity="top"/>
    <Button
        android:layout_
        android:layout_height="wrap_content"
        android:id="@+id/button2"
        android:text="button2"
        android:layout_gravity="center_vertical"/>
    <Button
        android:layout_
        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,重新運行程式,效果如圖:

image

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_
    android:layout_height="match_parent">
    <EditText
        android:id="@+id/input_message"
        android:layout_
        android:layout_height="wrap_content"
        android:layout_weight="1"
        android:hint="Type something"
        />
    <Button
        android:id="@+id/send"
        android:layout_
        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將在水平方向平分寬度,

image

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

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

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

image

還可以通過指定部分控制元件的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_
    android:layout_height="match_parent">
    <EditText
        android:id="@+id/input_message"
        android:layout_
        android:layout_height="wrap_content"
        android:layout_weight="1"
        android:hint="Type something"
        />
    <Button
        android:id="@+id/send"
        android:layout_
        android:layout_height="wrap_content"
        android:text="Send"/>

</LinearLayout>

這里僅指定了EditText的android:layout_weight屬性,并將Button的寬度改回wrap_content,這表示Button的寬度仍然按照wrap_content來計算,而EditText則會占滿螢屏所有的剩余空間,(

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

image

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_
    android:layout_height="match_parent">
    <Button
        android:layout_
        android:layout_height="wrap_content"
        android:id="@+id/button1"
        android:layout_alignParentLeft="true"
        android:layout_alignParentTop="true"
        android:text="Button1"/>
    <Button
        android:layout_
        android:layout_height="wrap_content"
        android:id="@+id/button2"
        android:layout_alignParentTop="true"
        android:layout_alignParentRight="true"
        android:text="Button2"/>
    <Button
        android:layout_
        android:layout_height="wrap_content"
        android:id="@+id/button3"
        android:layout_centerInParent="true"
        android:text="Button3"/>
    <Button
        android:layout_
        android:layout_height="wrap_content"
        android:id="@+id/button4"
        android:layout_alignParentBottom="true"
        android:layout_alignParentLeft="true"
        android:text="Button4"/>
    <Button
        android:layout_
        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這幾個屬性它們的名字已經完全說明了它們的作用,

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

image

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

<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_
    android:layout_height="match_parent">
    <Button
        android:layout_
        android:layout_height="wrap_content"
        android:id="@+id/button3"
        android:layout_centerInParent="true"
        android:text="Button3"/>
    <Button
        android:layout_
        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_
        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_
        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_
        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的情況,重新運行程式,效果如圖:

image

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_
    android:layout_height="match_parent">
    <TextView
        android:layout_
        android:layout_height="wrap_content"
        android:id="@+id/text_view"
        android:text="This is TextView"/>
    <ImageView
        android:layout_
        android:layout_height="wrap_content"
        android:id="@+id/image_view"
        android:src="https://www.cnblogs.com/1693977889zz/p/@mipmap/ic_launcher"/>
</FrameLayout>

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

image

可以看到,文字和圖片都是位于布局的左上角,由于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_
    android:layout_height="match_parent">
    <TextView
        android:layout_
        android:layout_height="wrap_content"
        android:id="@+id/text_view"
        android:layout_gravity="left"
        android:text="This is TextView"/>
    <ImageView
        android:layout_
        android:layout_height="wrap_content"
        android:id="@+id/image_view"
        android:layout_gravity="right"
        android:src="https://www.cnblogs.com/1693977889zz/p/@mipmap/ic_launcher"/>
</FrameLayout>

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

image

總體來講,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都會彈出一個如圖:

image

這個提示告訴我們,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_
    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中看到一些錯誤提示:

image

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

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

image

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

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

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

image

可以看到,所用的所有控制元件都是直接或間接繼承自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_
    android:layout_height="wrap_content"
    android:background="@drawable/title_bg">
    <Button
        android:layout_
        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_
        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_
        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_
    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()方法將標題欄隱藏起來,效果如圖:

image

使用這種方式,不管有多少布局需要添加標題欄,只需一行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_
    android:layout_height="match_parent">
    <com.zhouzhou.uicustomviews.TitleLayout
        android:layout_
        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()方法給兩個按鈕注冊了點擊事件,當點擊回傳按鈕時銷毀掉當前的活動,當點擊編輯按鈕時彈出一段文本,重新運行程式,點擊一下編輯按鈕,效果如圖:

image

這樣,每當我們在一個布局中引入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_
    android:layout_height="match_parent">
    <ListView
        android:layout_
        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 = https://www.cnblogs.com/1693977889zz/p/{"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和資料之間的關聯就建立完成了,

現在運行一下程式,效果如圖,可以通過滾動的方式來查看螢屏外的資料:

image

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_
    android:layout_height="wrap_content">
    <ImageView
        android:layout_
        android:layout_height="wrap_content"
        android:id="@+id/fruit_image"/>
    <TextView
        android:layout_
        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界面的任務就完成了,現在重新運行程式,效果如圖所示:

image

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將水果的名字顯示出來,重新運行程式,并點擊一下橘子,效果如圖:

image

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

image

  1. 編輯activity_main.xml代碼:
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_
    android:layout_height="match_parent">

    <androidx.recyclerview.widget.RecyclerView
        android:id="@+id/recycler_view"
        android:layout_
        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和資料之間的關聯就建立完成了,現在可以運行一下程式了,效果如圖:

image

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_
    android:layout_height="wrap_content">
    <ImageView
        android:layout_
        android:layout_height="wrap_content"
        android:id="@+id/fruit_image"
        android:layout_gravity="center_horizontal"/>
    <TextView
        android:layout_
        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就可以橫向滾動了,重新運行一下程式,效果如圖:

image

可以用手指在水平方向上滑動來查看螢屏外的資料,

為什么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_
    android:layout_height="wrap_content"
    android:layout_margin="5dp">
    <ImageView
        android:layout_
        android:layout_height="wrap_content"
        android:id="@+id/fruit_image"
        android:layout_gravity="center_horizontal"/>
    <TextView
        android:layout_
        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()這個方法來生成,這樣就能保證各水果名字的長短差距都比較大,子項的高度也就各不相同了,現在重新運行一下程式,效果如圖:

image

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

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的點擊事件,

image

再點擊菠蘿的文字部分,由于TextView并沒有注冊點擊事件,因此點擊文字這個事件會被子項的最外層布局捕獲到,效果如圖:

image

3.7 撰寫界面的最佳實踐

這次要綜合運用前面所學的大量內容來撰寫出一個較為復雜且相當美觀的聊天界面,要先創建一個UIBestPractice專案,

3.7.1 制作Nine-Patch圖片

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

image

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

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

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

image

可以看到,由于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 里直接打開編輯了,如圖所示:

image

點擊 Create9-Patch file...后,message_left.9.png被加載進來了:

image

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

image

(書中所寫:“最后點擊導航欄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"沒有更改)重新運行程式,效果如圖:

image

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

3.7.2 撰寫精美的聊天界面

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

image

接下來開始撰寫主界面,修改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_
    android:layout_height="match_parent"
    android:background="#d8e0e8">
    <androidx.recyclerview.widget.RecyclerView
        android:id="@+id/msg_recycler_view"
        android:layout_
        android:layout_height="0dp"
        android:layout_weight="1"/>
    <LinearLayout
        android:layout_
        android:layout_height="wrap_content">
        <EditText
            android:id="@+id/input_text"
            android:layout_
            android:layout_height="wrap_content"
            android:layout_weight="1"
            android:hint="Type something here"
            android:maxLines="2" />
        <Button
            android:layout_
            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_
    android:layout_height="wrap_content"
    android:padding="10dp">
    <LinearLayout
        android:id="@+id/left_layout"
        android:layout_
        android:layout_height="wrap_content"
        android:layout_gravity="left"
        android:background="@drawable/message_left">
        <TextView
            android:layout_
            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_
        android:layout_height="wrap_content"
        android:layout_gravity="right"
        android:background="@drawable/message_right">
        <TextView
            android:layout_
            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()方法將輸入的內容清空,

這樣所有的作業就都完成了,終于可以檢驗一下我們的成果了,運行程式之后你將會看到非常美觀的聊天界面,并且可以輸入和發送訊息,如圖:

image
image
image

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

轉載請註明出處,本文鏈接:https://www.uj5u.com/yidong/472331.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