本篇文章主要介绍以下几个知识点:
- 百分比布局;
- 引入布局,自定义控件;
- RecyclerView 的用法;
- 制作 Nine_Patch 图片;
- 实战 实现一个聊天界面。
3.1 百分比布局
百分比布局属于新增布局,在这种布局中,我们可以不再使用 wrap_content、match_parent等方式来指定控件的大小,而是允许直接指定控件布局中所占的百分比,可以轻松实现平分布局甚至任意比例分割布局的效果。
百分比布局只为FrameLayout 和 RelativeLayout 进行功能扩展,提供了 PercentFrameLayout 和 PercentRelativeLayout 这两个全新的布局。
用法:在项目的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中的布局代码,如下所示:
最外层使用了PercentFrameLayout,由于百分比布局并不是内置在系统 SDK 当中的,所以需要把完整路径写下来。然后定义一个app 的命名空间,方可使用百分比布局的自定义属性。PercentFrameLayout继承了FrameLayout的特性。
上述代码中定义了4个按钮,使用app:layout_widthPercent 属性将各按钮的宽度指定为布局的50%,使用app:layout_heightPercent属性将各按钮的高度指定为布局的50%,效果如图:
可以看到,每一个按钮的宽高都占据了布局的50%,轻松实现了4个按钮平分屏幕的效果。
另外一个PercentRelativeLayout的用法类似,继承了RelativeLayout中的所有属性,并可以使用app:layout_widthPercent 和app:layout_heightPercent来按百分比指定控件的宽高。
3.2 创建自定义控件
我们所用的所有控件都是直接或间接继承自 View 的,所用的所有布局都是直接或间接继承自 ViewGroup 的。View 是 Android 中一种最基本的 UI 组件,它可以在屏幕 上绘制一块矩形区域,并能响应这块区域的各种事件,因此,我们使用的各种控件其实就是 在 View 的基础之上又添加了各自特有的功能。而 ViewGroup 则是一种特殊的 View,它可以 包含很多的子 View 和子 ViewGroup,是一个用于放置控件和布局的容器。
如图所示:
3.2.1 引入布局
来实现个类似iPhone 应用的界面顶部的标题栏, 标题栏上有两个按钮可用于返回或其他操作(iPhone 没有实体返回键)。
新建个布局title.xml,代码如下:
在 LinearLayout 中分别加入了两个 Button 和一个 TextView。
现在标题栏布局已经编写完了,剩下的就是如何在程序中使用这个标题栏了,修改activity_custom_title.xml 中的代码,如下所示:
只需要通过一行 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 中的代码,如下所示:
重新运行程序,效果如图所示:
这样的话,每当我们在一个布局中引入 TitleLayout,省去了很多编写重复代码的工作。
3.3 强大的滚动控件——RecyclerView
RecyclerView 可以说是一个增强版的 ListView,不仅可以轻松实现ListView 同样的效果,还优化了 ListView 中存在的各种不足。接下来讲解下 RecyclerView 的用法。
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中的代码:
我们的目的是为了用 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:
接下来为 RecyclerView 准备一个适配器,新建 FruitAdapter 类,让这个适配器继承RecyclerView.Adapter,并将泛型指定为 FruitAdapter.ViewHolder。其中,ViewHolder 是我们在 FruitAdapter 中定义的一个内部类,代码如下:
/**
* 水果适配器
* Created by KXwon on 2016/12/11.
*/
public class FruitAdapter extends RecyclerView.Adapter{
private List mFruitList;
/**
* 构造函数,用于把要展示的数据源传进来
* @param mFruitList
*/
public FruitAdapter(List 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 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);
}
}
}
运行效果如下:
3.3.2 实现横向滚动和瀑布流布局
用 RecyclerView 实现横向滚动效果,修改 fruit_item.xml 中的代码:
上述代码中,把 LinearLayout 改成了垂直方向,宽度设为 100dp,把 ImageView 和 TextView 设成了布局中水平居中,接下来修改 activity 中的代码:
public class RecyclerVewActivity extends AppCompatActivity {
private List 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 中只加了一行代码,调用 LinearLayoutManager 的 setOrientation() 方法来设置布局的排列方向,运行程序,效果如下:
除了 LinearLayoutManager 之外,RecyclerView 还提供了 GridLayoutManager 和 StaggeredGridLayoutManager 两种内置的布局排列方式。GridLayoutManager 实现网格布局,StaggeredGridLayoutManager 实现瀑布流布局。
接下来实现下瀑布流布局,首先修改 fruit_item.xml 中的代码:
上述代码中,把 LinearLayout 的宽度设为 match_parent 因为瀑布流布局的宽度是根据布局的列数来自动适配的,而不是一个固定值,把 TextView 设成了居左对齐,接下来修改 activity 中的代码:
public class RecyclerVewActivity extends AppCompatActivity {
private List 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{
private List mFruitList;
/**
* 构造函数,用于把要展示的数据源传进来
* @param mFruitList
*/
public FruitAdapter(List 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,如图所示
若将这张图片设置为一个 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 加载进来,如图所示:
我们可以在图片的四个边框绘制一个个的小黑点,在上边框和左边框绘制的部分就表示当图片需要拉伸时就拉伸黑点标记的区域,在下边框和右边框绘制的部分则表示内容会被放置的区域。使用鼠标在图片的边缘拖动就可以绘制了,按住 Shift 键拖动可以进行擦除,完成后效果如图所示:
最后保存即可。用制作好的图片替换掉之前的 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 中的代码如下:
上述代码在主界面放置了一个 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,如下:
接下来创建 RecyclerView 的适配器,新建 MsgAdapter,如下:
/**
* 消息适配器
* Created by KXwon on 2016/12/11.
*/
public class MsgAdapter extends RecyclerView.Adapter{
private List mMsgList;
public MsgAdapter(List 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 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);
}
}
这样一个可以输入和发送消息的聊天界面所有的工作就都完成了,运行程序,效果如下:
至此,第三章笔记就到这,下篇文章将学习碎片的知识。