第一行代码读书笔记 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阅读 194,390评论 5 459
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 81,821评论 2 371
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 141,632评论 0 319
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 52,170评论 1 263
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 61,033评论 4 355
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 46,098评论 1 272
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 36,511评论 3 381
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 35,204评论 0 253
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 39,479评论 1 290
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 34,572评论 2 309
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 36,341评论 1 326
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 32,213评论 3 312
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 37,576评论 3 298
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 28,893评论 0 17
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,171评论 1 250
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 41,486评论 2 341
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 40,676评论 2 335

推荐阅读更多精彩内容