第一行代碼讀書筆記 3 -- UI 開發

本篇文章主要介紹以下幾個知識點:

  • 百分比布局;
  • 引入布局,自定義控件;
  • RecyclerView 的用法;
  • 制作 Nine_Patch 圖片;
  • 實戰 實現一個聊天界面。
圖片來源于網絡

3.1 百分比布局

百分比布局屬于新增布局,在這種布局中,可以不再使用 wrap_contentmatch_parent 等方式來指定控件的大小,而是允許直接指定控件布局中所占的百分比,可以輕松實現平分布局甚至任意比例分割布局的效果。
??
百分比布局只為 FrameLayoutRelativeLayout 進行功能擴展,提供了 PercentFrameLayoutPercentRelativeLayout 這兩個全新的布局。
??
用法:在項目的 build.gradle 中添加百分比布局庫的依賴:


dependencies {
    compile fileTree(dir: 'libs', include: ['*.jar'])
    compile 'com.android.support:appcompat-v7:24.2.1'
    compile 'com.android.support:percent:24.2.1'
    testCompile 'junit:junit:4.12'
}

接下來修改 activity_percent.xml 中的布局代碼,如下:

<android.support.percent.PercentFrameLayout
    xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    android:id="@+id/activity_percent"
    android:layout_width="match_parent"
    android:layout_height="match_parent">
    <Button
        android:id="@+id/button1"
        android:text="button1"
        android:layout_gravity="left|top"
        app:layout_widthPercent="50%"
        app:layout_heightPercent="50%"/>

    <Button
        android:id="@+id/button2"
        android:text="button2"
        android:layout_gravity="right|top"
        app:layout_widthPercent="50%"
        app:layout_heightPercent="50%"/>

    <Button
        android:id="@+id/button3"
        android:text="button3"
        android:layout_gravity="left|bottom"
        app:layout_widthPercent="50%"
        app:layout_heightPercent="50%"/>

    <Button
        android:id="@+id/button4"
        android:text="button4"
        android:layout_gravity="right|bottom"
        app:layout_widthPercent="50%"
        app:layout_heightPercent="50%"/>

</android.support.percent.PercentFrameLayout>

最外層使用了 PercentFrameLayout,由于百分比布局并不是內置在系統 SDK 當中的,所以需要把完整路徑寫下來。然后定義一個 app 的命名空間,方可使用百分比布局的自定義屬性。PercentFrameLayout 繼承了 FrameLayout 的特性。
??
上面定義了4個按鈕,使用 app:layout_widthPercentapp:layout_heightPercent 屬性將各按鈕的寬度、高度指定為布局的50%,效果如圖:

PercentFrameLayout運行效果

可以看到,每一個按鈕的寬高都占據了布局的50%,輕松實現了4個按鈕平分屏幕的效果。

另外一個 PercentRelativeLayout 的用法類似,繼承了 RelativeLayout 中的所有屬性,并可以使用app:layout_widthPercentapp:layout_heightPercent 來按百分比指定控件的寬高。

3.2 創建自定義控件

我們所用的所有控件都是直接或間接繼承自 View 的,所用的所有布局都是直接或間接繼承自 ViewGroup 的。

ViewAndroid 中一種最基本的 UI 組件,它可以在屏幕上繪制一塊矩形區域,并能響應這塊區域的各種事件,也就是說各種控件其實就是在 View 的基礎之上又添加了各自特有的功能。

ViewGroup 則是一種特殊的 View,它可以包含很多的子 View子 ViewGroup,是一個用于放置控件和布局的容器。
??
如圖所示:

常用控件和布局的繼承結構

3.2.1 引入布局

來實現個類似 iPhone 應用的界面頂部的標題欄, 標題欄上有兩個按鈕可用于返回或其他操作(iPhone 沒有實體返回鍵)。
??
新建個布局 title.xml 如下:

<LinearLayout
    xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:background="@color/colorAccent" >

    <Button
        android:id="@+id/title_back"
        android:layout_width="60dp"
        android:layout_height="60dp"
        android:layout_gravity="center"
        android:layout_margin="5dip"
        android:background="@drawable/bg_back"
        android:textColor="#fff" />

    <TextView
        android:id="@+id/title_text"
        android:layout_width="0dip"
        android:layout_height="wrap_content"
        android:layout_gravity="center"
        android:layout_weight="1"
        android:gravity="center"
        android:text="Title Text"
        android:textColor="#fff"
        android:textSize="24sp" />

    <Button
        android:id="@+id/title_edit"
        android:layout_width="60dp"
        android:layout_height="60dp"
        android:layout_gravity="center"
        android:layout_margin="5dip"
        android:background="@drawable/bg_message"
        android:textColor="#fff" />

</LinearLayout>

LinearLayout 中分別加入了兩個 Button 和一個 TextView
??
如何在程序中使用這個標題欄布局,修改 activity_custom_title.xml 中的代碼如下:

<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 語句將標題欄布局引入進來即可。 最后別忘了在 CustomTitleActivity 中將系統自帶的標題欄隱藏掉,如下:

public class CustomTitleActivity extends AppCompatActivity {

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_custom_title);
        ActionBar actionBar = getSupportActionBar();
        if (actionBar != null){
            actionBar.hide(); // 隱藏系統自帶的標題欄
        }
    }
}

運行程序,效果如下:

引入標題欄布局的效果

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

3.2.2 創建自定義控件

引入布局的技巧解決了重復編寫布局代碼的問題,但若布局中有一些控件要求能夠響應事件,還需要在每個活動中為這些控件單獨編寫一次事件注冊的代碼。如標題欄中的返回按鈕,其實不管是在哪一個活動中,這個按鈕的功能都是相同的,即銷毀掉當前活動。

而如果在每一個活動中都需要重新注冊一遍返回按鈕的點擊事件,無疑又增加了很多重復代碼,此時最好是使用自定義控件的方式來解決。

新建 TitleLayout 繼承自 LinearLayout,讓它成為自定義的標題欄控件,代碼如下:

/**
 * 自定義標題欄
 * Created by KXwon on 2016/12/9.
 */
public class TitleLayout extends LinearLayout {

    public TitleLayout(Context context, AttributeSet attrs) {

        super(context, attrs);
        LayoutInflater.from(context).inflate(R.layout.title, this);
        
        // 初始化兩個按鈕
        Button titleBack = (Button) findViewById(R.id.title_back); 
        Button titleMessage = (Button) findViewById(R.id.title_message); 
        // 設置點擊事件
        titleBack.setOnClickListener(new OnClickListener() {
            @Override
            public void onClick(View v) { 
                // 點擊返回按鈕銷毀當前活動
                ((Activity) getContext()).finish();
            }
        });

        titleMessage.setOnClickListener(new OnClickListener() {
            @Override
            public void onClick(View v) {
                Toast.makeText(getContext(), "You clicked Message button", Toast.LENGTH_SHORT).show();
            }
        });
    }
}

現在自定義控件已經創建好了,然后需要在布局文件中添加這個自定義控件,修改 activity_custom_title.xml 中的代碼如下:

<LinearLayout
   xmlns:android="http://schemas.android.com/apk/res/android"
   android:layout_width="match_parent"
   android:layout_height="match_parent" >

   <com.wonderful.myfirstcode.custom_controls.TitleLayout
       android:layout_width="match_parent"
       android:layout_height="match_parent"/>

</LinearLayout>

重新運行程序,效果如圖所示:

自定義標題欄效果

這樣每當在一個布局中引入 TitleLayout,省去了很多編寫重復代碼的工作。

3.3 強大的滾動控件——RecyclerView

RecyclerView 可以說是一個增強版的 ListView,不僅可以輕松實現 ListView 同樣的效果,還優化了 ListView 中存在的各種不足。

3.3.1 RecyclerView 的基本用法

使用 RecyclerView 這個控件,首先需要在項目的 build.gradle 中添加相應的依賴庫才行。

打開 app/build.gradle 文件,在 dependencies 閉包中添加如下內容:

dependencies {
    compile fileTree(dir: 'libs', include: ['*.jar'])
    compile 'com.android.support:appcompat-v7:24.2.1'
    compile 'com.android.support:recyclerview-v7:24.2.1'
    testCompile 'junit:junit:4.12'
}

然后修改 activity_recycler_view.xml 中的代碼:

<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:id="@+id/activity_recycler_vew"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context="com.wonderful.myfirstcode.custom_controls.recycler_view.RecyclerVewActivity">

    <android.support.v7.widget.RecyclerView
        android:id="@+id/recycler_view"
        android:layout_width="match_parent"
        android:layout_height="match_parent"/>
    
</LinearLayout>

下面用 RecyclerView 來展示一個水果列表,先建立一個水果 Fruit 類:

/**
 * 水果類
 * Created by KXwon on 2016/12/11.
 */

public class Fruit {
    private String name; // 水果名
    private int imageId; // 水果圖片id

    public Fruit(String name, int imageId) {
        this.name = name;
        this.imageId = imageId;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public int getImageId() {
        return imageId;
    }

    public void setImageId(int imageId) {
        this.imageId = imageId;
    }
}

以及展示水果的布局 fruit_item.xml

<LinearLayout 
    xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="wrap_content">

    <ImageView
        android:id="@+id/fruit_image"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"/>

    <TextView
        android:id="@+id/fruit_name"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_gravity="center_vertical"
        android:layout_marginLeft="10dp"/>

</LinearLayout>

接下來為 RecyclerView 準備一個適配器,新建 FruitAdapter 類,讓這個適配器繼承RecyclerView.Adapter,并將泛型指定為 FruitAdapter.ViewHolder。其中,ViewHolder 是在 FruitAdapter 中定義的一個內部類,代碼如下:

/**
 * 水果適配器
 * Created by KXwon on 2016/12/11.
 */

public class FruitAdapter extends RecyclerView.Adapter<FruitAdapter.ViewHolder>{

    private List<Fruit> mFruitList;

    /**
     * 構造函數,用于把要展示的數據源傳進來
     * @param mFruitList
     */
    public FruitAdapter(List<Fruit> mFruitList) {
        this.mFruitList = mFruitList;
    }

    /**
     * 創建ViewHolder實例
     * @param parent
     * @param viewType
     * @return
     */
    @Override
    public ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
        View view = LayoutInflater.from(parent.getContext()).inflate(R.layout.fruit_item,parent,false);
        ViewHolder holder = new ViewHolder(view);
        return holder;
    }

    /**
     * 對RecyclerView子項的數據進行賦值
     * @param holder
     * @param position
     */
    @Override
    public void onBindViewHolder(ViewHolder holder, int position) {
        Fruit fruit = mFruitList.get(position);
        holder.fruitImage.setImageResource(fruit.getImageId());
        holder.fruitName.setText(fruit.getName());
    }

    /**
     * 子項的數目
     * @return
     */
    @Override
    public int getItemCount() {
        return mFruitList.size();
    }

    /**
     * 內部類,ViewHolder要繼承自 RecyclerView.ViewHolder
     */
    public class ViewHolder extends RecyclerView.ViewHolder{
        ImageView fruitImage;
        TextView fruitName;

        public ViewHolder(View itemView) {
            super(itemView);
            fruitImage = (ImageView) itemView.findViewById(R.id.fruit_image);
            fruitName = (TextView) itemView.findViewById(R.id.fruit_name);
        }
    }
}

適配器準備好了之后,可以開始使用 RecyclerView 了,activity 中的代碼如下:

public class RecyclerVewActivity extends AppCompatActivity {

    private List<Fruit> fruitList = new ArrayList<>();

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

         // 初始化水果數據
        initFruits();
        // 獲取RecyclerView的實例
        RecyclerView recyclerView = (RecyclerView) findViewById(R.id.recycler_view);
        // LayoutManager用于指定RecyclerView的布局方式,LinearLayoutManager表示線性布局
        LinearLayoutManager layoutManager = new LinearLayoutManager(this);
        recyclerView.setLayoutManager(layoutManager);
        // 創建FruitAdapter的實例
        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.pic_apple);
            fruitList.add(apple);
            Fruit banana = new Fruit("Banana",R.drawable.pic_banana);
            fruitList.add(banana);
            Fruit orange = new Fruit("orange",R.drawable.pic_orange);
            fruitList.add(orange);
            Fruit watermelon = new Fruit("watermelon",R.drawable.pic_watermelon);
            fruitList.add(watermelon);
            Fruit grape = new Fruit("grape",R.drawable.pic_grape);
            fruitList.add(grape);
            Fruit pineapple = new Fruit("pineapple",R.drawable.pic_pineapple);
            fruitList.add(pineapple);
            Fruit strawberry = new Fruit("strawberry",R.drawable.pic_strawberry);
            fruitList.add(strawberry);
            Fruit cherry = new Fruit("cherry",R.drawable.pic_cherry);
            fruitList.add(cherry);
            Fruit mango = new Fruit("mango",R.drawable.pic_mango);
            fruitList.add(mango);
        }
    }
}

運行效果如下:

RecyclerView運行效果

3.3.2 實現橫向滾動和瀑布流布局

RecyclerView 實現橫向滾動效果,修改 fruit_item.xml 中的代碼:

<LinearLayout
    xmlns:android="http://schemas.android.com/apk/res/android"
    android:orientation="vertical"
    android:layout_width="100dp"
    android:layout_height="wrap_content">

    <ImageView
        android:id="@+id/fruit_image"
        android:layout_width="60dp"
        android:layout_height="60dp"
        android:padding="5dp"
        android:layout_gravity="center_horizontal"/>

    <TextView
        android:id="@+id/fruit_name"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_gravity="center_horizontal"
        android:layout_marginTop="10dp"/>

</LinearLayout>

上述代碼中,把 LinearLayout 改成了垂直方向,寬度設為 100dp,把 ImageViewTextView 設成了布局中水平居中,接下來修改 activity 中的代碼:

public class RecyclerVewActivity extends AppCompatActivity {

    private List<Fruit> fruitList = new ArrayList<>();

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

        // 初始化水果數據
        initFruits();
        // 獲取RecyclerView的實例
        RecyclerView recyclerView = (RecyclerView) findViewById(R.id.recycler_view);
        // LayoutManager用于指定RecyclerView的布局方式,LinearLayoutManager表示線性布局
        LinearLayoutManager layoutManager = new LinearLayoutManager(this);
        // 設置布局橫向排列(默認是縱向排列的)
        layoutManager.setOrientation(LinearLayoutManager.HORIZONTAL);
        recyclerView.setLayoutManager(layoutManager);
        // 創建FruitAdapter的實例
        FruitAdapter adapter = new FruitAdapter(fruitList);
        // 設置適配器
        recyclerView.setAdapter(adapter);
    }
    ...
}  

Activity 中只加了一行代碼,調用 LinearLayoutManagersetOrientation() 方法來設置布局的排列方向,運行程序,效果如下:

橫向 RecyclerView 效果

除了 LinearLayoutManager 之外,RecyclerView 還提供了 GridLayoutManagerStaggeredGridLayoutManager 兩種內置的布局排列方式。GridLayoutManager 實現網格布局,StaggeredGridLayoutManager 實現瀑布流布局。

接下來實現下瀑布流布局,首先修改 fruit_item.xml 中的代碼:

<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:id="@+id/fruit_image"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_gravity="center_horizontal"/>

    <TextView
        android:id="@+id/fruit_name"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_gravity="left"
        android:layout_marginTop="10dp"/>

</LinearLayout>

上述代碼中,把 LinearLayout 的寬度設為 match_parent 因為瀑布流布局的寬度是根據布局的列數來自動適配的,而不是一個固定值,把 TextView 設成了居左對齊,接下來修改 activity 中的代碼:

public class RecyclerVewActivity extends AppCompatActivity {

    private List<Fruit> fruitList = new ArrayList<>();

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

        // 初始化水果數據
        initFruits();
        // 獲取RecyclerView的實例
        RecyclerView recyclerView = (RecyclerView) findViewById(R.id.recycler_view);
        // 創建StaggeredGridLayoutManager的實例(構造函數中的兩個參數:第一個指定布局的列數,第二個指定布局的排列方向)
        StaggeredGridLayoutManager layoutManager = new StaggeredGridLayoutManager(3,StaggeredGridLayoutManager.VERTICAL);
        recyclerView.setLayoutManager(layoutManager);
        // 創建FruitAdapter的實例
        FruitAdapter adapter = new FruitAdapter(fruitList);
        // 設置適配器
        recyclerView.setAdapter(adapter);
    }

    private void initFruits() {
        for (int i = 0;i < 2;i++){
            for (int i = 0;i < 2;i++){
            Fruit apple = new Fruit(getRandomLengthName("Apple"),R.drawable.pic_apple);
            fruitList.add(apple);
            Fruit banana = new Fruit(getRandomLengthName("Banana"),R.drawable.pic_banana);
            fruitList.add(banana);
            Fruit orange = new Fruit(getRandomLengthName("orange"),R.drawable.pic_orange);
            fruitList.add(orange);
            Fruit watermelon = new Fruit(getRandomLengthName("watermelon"),R.drawable.pic_watermelon);
            fruitList.add(watermelon);
            Fruit grape = new Fruit(getRandomLengthName("grape"),R.drawable.pic_grape);
            fruitList.add(grape);
            Fruit pineapple = new Fruit(getRandomLengthName("pineapple"),R.drawable.pic_pineapple);
            fruitList.add(pineapple);
            Fruit strawberry = new Fruit(getRandomLengthName("strawberry"),R.drawable.pic_strawberry);
            fruitList.add(strawberry);
            Fruit cherry = new Fruit(getRandomLengthName("cherry"),R.drawable.pic_cherry);
            fruitList.add(cherry);
            Fruit mango = new Fruit(getRandomLengthName("mango"),R.drawable.pic_mango);
            fruitList.add(mango);
        }
    }

    /**
     * 隨機生成水果名字的長度
     * @param name
     * @return
     */
    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();
    }
}

至此,已成功實現瀑布流效果了,效果如下:

瀑布流布局效果

3.3.3 RecyclerView 的點擊事件

不同于 ListView 的是,RecyclerView 并沒有提供類似 setOnItemClickListener() 這樣的注冊監聽方法,而是需要給子項具體的 view 去注冊點擊事件。
??
為實現 RecyclerView 中注冊點擊事件,修改 FruitAdapter 中的代碼:

/**
 * 水果適配器
 * Created by KXwon on 2016/12/11.
 */

public class FruitAdapter extends RecyclerView.Adapter<FruitAdapter.ViewHolder>{

    private List<Fruit> mFruitList;

    /**
     * 構造函數,用于把要展示的數據源傳進來
     * @param mFruitList
     */
    public FruitAdapter(List<Fruit> mFruitList) {
        this.mFruitList = mFruitList;
    }

    /**
     * 創建ViewHolder實例
     * @param parent
     * @param viewType
     * @return
     */
    @Override
    public ViewHolder onCreateViewHolder(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);
                ToastUtils.showShort("you clicked view"+ fruit.getName());
            }
        });
        // 為ImageView注冊點擊事件
        holder.fruitImage.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View view) {
                int position = holder.getAdapterPosition();
                Fruit fruit = mFruitList.get(position);
                ToastUtils.showShort("you clicked image"+ fruit.getName());
            }
        });
        return holder;
    }

    /**
     * 對RecyclerView子項的數據進行賦值
     * @param holder
     * @param position
     */
    @Override
    public void onBindViewHolder(ViewHolder holder, int position) {
        Fruit fruit = mFruitList.get(position);
        holder.fruitImage.setImageResource(fruit.getImageId());
        holder.fruitName.setText(fruit.getName());
    }

    /**
     * 子項的數目
     * @return
     */
    @Override
    public int getItemCount() {
        return mFruitList.size();
    }

    /**
     * 內部類,ViewHolder要繼承自 RecyclerView.ViewHolder
     */
    public class ViewHolder extends RecyclerView.ViewHolder{
        View fruitView; // 添加fruitView變量來保存子項最外層布局的實例
        ImageView fruitImage;
        TextView fruitName;

        public ViewHolder(View itemView) {
            super(itemView);
            fruitView = itemView;
            fruitImage = (ImageView) itemView.findViewById(R.id.fruit_image);
            fruitName = (TextView) itemView.findViewById(R.id.fruit_name);
        }
    }
}

上述代碼,先修改了 ViewHolder,在 ViewHolder 中添加了 fruitView 變量來保存子項最外層布局的實例,然后在 onCreateViewHolder() 方法中注冊點擊事件就可以了。

這里分別為最外層布局和 ImageView 注冊了點擊事件。RecyclerView 的強大之處在于可以輕松實現子項中任意控件或布局的點擊事件。
??
運行程序,并點擊香蕉的圖片部分,效果如下:

點擊香蕉的圖片部分

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

點擊菠蘿的文字部分

3.4 編寫界面的最佳實踐

3.4.1 制作 Nine_Patch 圖片

若項目中有一張氣泡樣式的圖片 message_left.png,如圖所示:

message_left.png

若將這張圖片設置為一個 LinearLayout 的背景圖片,代碼如下:

<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 圖片來進行改善。

Android sdk 目錄下有一個 tools 文件夾,在這個文件夾中找到 draw9patch.bat 文件, 可使用它來制作 Nine-Patch 圖片。

雙擊打開 draw9patch.bat 文件,在導航欄點擊 File→Open 9-patch 將準備好的圖片 message_left.png 加載進來,如圖所示:

使用 draw9patch 編輯 message_left 圖片

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

繪制完后的 message_left 圖片

最后保存即可。用制作好的圖片替換掉之前的 message_left.png 圖片,重新運行程序,效果如圖:

氣泡只拉伸繪制區域的效果

接下來進入實戰環節。

3.4.2 編寫精美的聊天界面

上面制作的 message_left.9.png 可以作為收到消息的背景圖,再制作一張 message_right.9.png 作為發出消息的背景圖。
??
首先在 app/buiild.gradle 當中添加要用到的 RecyclerView 依賴庫:

dependencies {
    compile fileTree(dir: 'libs', include: ['*.jar'])
    compile 'com.android.support:appcompat-v7:24.2.1'
    compile 'com.android.support:recyclerview-v7:24.2.1'
    testCompile 'junit:junit:4.12'
}

接下來編寫主界面,編寫主界面 activity_ui_best_practice.xml 中的代碼如下:

<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="vertical"
    android:background="#d8e0e8">
    
    <android.support.v7.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/et_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:id="@+id/btn_send"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:text="Send" />
        
    </LinearLayout>

</LinearLayout>

上述代碼在主界面放置了一個 RecyclerView 來顯示聊天的消息內容,放置了一個 EditText 用于輸入消息,放置了一個 Button 用于發送消息。
??
然后定義消息的實體類 Msg 如下所示:

/**
 * 消息實體類
 * Created by KXwon on 2016/12/11.
 */

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 void setContent(String content) {
        this.content = content;
    }

    public int getType() {
        return type;
    }

    public void setType(int type) {
        this.type = type;
    }
}

接著編寫 RecyclerView 子項的布局,新建 msg_item.xml,如下:

<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:orientation="vertical"
    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:id="@+id/left_msg"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            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:id="@+id/right_msg"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_gravity="center"
            android:layout_margin="10dp" />

    </LinearLayout>

</LinearLayout>

接下來創建 RecyclerView 的適配器,新建 MsgAdapter,如下:

/**
 * 消息適配器
 * Created by KXwon on 2016/12/11.
 */

public class MsgAdapter extends RecyclerView.Adapter<MsgAdapter.ViewHolder>{

    private List<Msg> mMsgList;

    public MsgAdapter(List<Msg> mMsgList) {
        this.mMsgList = mMsgList;
    }

    @Override
    public ViewHolder onCreateViewHolder(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(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.leftLayout.setVisibility(View.GONE);
            holder.rightLayout.setVisibility(View.VISIBLE);
            holder.rightMsg.setText(msg.getContent());
        }
    }

    @Override
    public int getItemCount() {
        return mMsgList.size();
    }

    static class ViewHolder extends RecyclerView.ViewHolder{

        LinearLayout leftLayout, rightLayout;

        TextView leftMsg, rightMsg;

        public ViewHolder(View view) {
            super(view);
            leftLayout = (LinearLayout) view.findViewById(R.id.left_layout);
            rightLayout = (LinearLayout) view.findViewById(R.id.right_layout);
            leftMsg = (TextView) view.findViewById(R.id.left_msg);
            rightMsg = (TextView) view.findViewById(R.id.right_msg);
        }
    }

}

最后修改 activity 中的代碼,來為 RecyclerView 初始化一些數據,并給發送消息加入事件響應,如下:

public class UIBestPracticeActivity extends AppCompatActivity {

    private List<Msg> msgList = new ArrayList<>();

    private EditText et_input_text;
    private Button btn_send;

    private RecyclerView msgRecyclerView;
    private MsgAdapter adapter;

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

        initMsg(); // 初始化消息數據

        et_input_text = (EditText) findViewById(R.id.et_input_text);
        btn_send = (Button) findViewById(R.id.btn_send);

        msgRecyclerView = (RecyclerView) findViewById(R.id.msg_recycler_view);
        LinearLayoutManager layoutManager = new LinearLayoutManager(this);
        msgRecyclerView.setLayoutManager(layoutManager);
        adapter = new MsgAdapter(msgList);
        msgRecyclerView.setAdapter(adapter);

        btn_send.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View view) {
                String content = et_input_text.getText().toString();
                if (!"".equals(content)){
                    Msg msg = new Msg(content,Msg.TYPE_SENT);
                    msgList.add(msg);
                    // 當有新消息時,刷新RecyclerView中的顯示
                    adapter.notifyItemInserted(msgList.size() - 1);
                    // 將RecyclerView定位到最后一行
                    msgRecyclerView.scrollToPosition(msgList.size() - 1);
                    // 清空輸入框中的內容
                    et_input_text.setText("");
                }
            }
        });
    }

    private void initMsg() {
        Msg msg1 = new Msg("Hello world!",Msg.TYPE_RECEIVED);
        msgList.add(msg1);
        Msg msg2 = new Msg("Hello. Who is that?",Msg.TYPE_SENT);
        msgList.add(msg2);
        Msg msg3 = new Msg("。。。",Msg.TYPE_SENT);
        msgList.add(msg3);
        Msg msg4 = new Msg("This is 逗逼. Nice talking to you",Msg.TYPE_RECEIVED);
        msgList.add(msg4);
    }
}

這樣一個可以輸入和發送消息的聊天界面所有的工作就都完成了,運行效果如下:

聊天界面

至此,第三章筆記就到這,下篇文章將學習碎片的知識。

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 228,333評論 6 531
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 98,491評論 3 416
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 176,263評論 0 374
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 62,946評論 1 309
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 71,708評論 6 410
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 55,186評論 1 324
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,255評論 3 441
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,409評論 0 288
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 48,939評論 1 335
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 40,774評論 3 354
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 42,976評論 1 369
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,518評論 5 359
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,209評論 3 347
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,641評論 0 26
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,872評論 1 286
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 51,650評論 3 391
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 47,958評論 2 373

推薦閱讀更多精彩內容