文章目录
如何编写程序页面
编写XML代码实现界面。
常用控件的使用方法
TextView
主要用于在桌面上显示一段信息
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:orientation="vertical"
android:layout_width="match_parent"
android:layout_height="match_parent">
<TextView
android:id="@+id/text_view"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="This is TextView" />
</LinearLayout>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:orientation="vertical"
android:layout_width="match_parent"
android:layout_height="match_parent">
<TextView
android:id="@+id/text_view"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:gravity="center"
android:text="This is TextView" />
</LinearLayout>
我们使用android:gravity
来指定文字的对齐方式,可选值有top、bottom、left、right、center等,可以用“1” 来同时指定多个值,这里我们指定的center,效果等同于center_vertical Icenter_ horizontal
, 表示文字在垂直和水平方向都居中对齐。现在重新运行程序,
修改文字的大小和颜色
通过android: textSize
属性可以指定文字的大小,通过android: textColor
属性可以指定文字的颜色,在Android中字体大小使用sp作为单位。
Button
Button是程序用于和用户进行交互的一个重要控件,它可配置的属性和TextView差不多
<Button
android:id="@+id/button"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="Button"/>
系统会对Button中的所有英文字母自动进行大写转化,禁用这一默认特性代码如下
android:textAllCaps="false"
为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 v) {
//在此处编辑
}
});
}
}
每当点击按钮时,就会执行监听器中的onClick()
方法,我们只需要在这个方法中加入自己的逻辑就行,
也可以使用实现接口的方式进行注册
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:
//在此添加编辑
break;
default:
break;
}
}
}
EditText
EditText是程序用于和用户进行交互的另一个重要控件,它允许用户在控件里输人和编辑内容,并可以在程序中对这些内容进行处理。EditText的应用场景非常普遍,在进行发短信、发微博、聊QQ等操作时,你不得不使用EditText。那我们来看一看如何在界面上加入EditText 吧,修改activity_main.xml
中的代码,如下所示
<EditText
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:id="@+id/edit_text"/>
细心的你平时应该会留意到,一些做得比较人性化的软件会在输人框里显示一些提示性的文字,然后一旦用户输入了任何内容,这些提示性的文字就会消失。这种提示功能在Android里是非常容易实现的,我们甚至不需要做任何的逻辑控制,因为系统已经帮我们都处理好了。修改activity_ main.xml, 如下所示:
<EditText
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:id="@+id/edit_text"
android:hint="Type something here"/>
不过,随着输人的内容不断增多,EditText 会被不断地拉长。这时由于EditText 的高度指定的是wrap_ content
因此它总能包含住里面的内容,但是当输人的内容过多时,界面就会变得非常难看。我们可以使android:maxLines
属性来解决这个问题,修改activity_ main.xml, 如下所示:
<EditText
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:id="@+id/edit_text"
android:hint="Type something here"
android:maxLines="2"/>
这里通过android: maxL ines指定了EditText 的最大行数为两行,这样当输人的内容超过两行时,文本就会向上滚动,而EditText则不会再继续拉伸.
我们还可以结合使用EditText 与Button来完成一些功能,比如通过点击按钮来获取EditText中输人的内容。修改MainActivity中的代码,如下所示:
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);
}
@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;
}
}
}
首先通过findViewById()
方法得到EditText的实例,然后在按钮的点击事件里调用EditText的getText()
方法获取到输人的内容,再调用toString()
方法转换成字符串,最后还是老方法,使用Toast将输人的内容显示出来。
ImageView
ImageView是用于在界面上展示图片的一个控件,它可以让我们的程序界面变得更加丰富多彩。学习这个控件需要提前准备好一些图片,这里我们在res目录下新建一个drawable-xhdpi目录,然后将事先准备好的两张图片img_ 1.png 和img_ 2.png 复制到该目录当中。
<ImageView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:id="@+id/image_view"
android:src="@drawable/img_1"
/>
可以看到,这里使用android:src属性给ImageView指定了一张图片。由于图片的宽和高都是未知的,所以将ImageView的宽和高都设定为wrap_content, 这样就保证了不管图片的尺寸是多少,图片都可以完整地展示出来。
我们还可以在程序中通过代码动态地更改ImageView中的图片,然后修改MainActivity 的代码,如下所示:
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 = (Button) findViewById(R.id.button);
//获取EditText实例
editText = (EditText) findViewById(R.id.edit_text);
//获取imageView实例
imageView = (ImageView) findViewById(R.id.image_view);
button.setOnClickListener(this);
}
@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中显示的图片改变了,
ProgressBar
ProgressBar用于在界面上显示一个进度条,表示我们的程序正在加载一些数据。它的用法也非常简单,修改activity_ main.xml
中的代码,如下所示:
<ProgressBar
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:id="@+id/progress_bar"/>
让进度条在数据加载完成时消失,需要用到Android控件的可见属性。所有的Android控件都具有这个属性,可以通过android:visibility
进行指定,可选值有3种: visible
、 invisible
和gone
。
- visible 表示控件是可见的,这个值是默认值,不指定
android:visibility
时,控件都是可见的。 - invisible 表示控件不可见,但是它仍然占据着原来的位置和大小,可以理解成控件变成透明状态了。
- gone则表示控件不仅不可见,而且不再占用任何屏幕空间。
我们还可以通过代码来设置控件的可见性,使用的是setVisibility()
方法,可以传入View. VISIBLE、View. INVISIBLE和View. GONE这3种值。
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 = (Button) findViewById(R.id.button);
//获取EditText实例
editText = (EditText) findViewById(R.id.edit_text);
//获取imageView实例
imageView = (ImageView) findViewById(R.id.image_view);
//获取progressBar实例
progressBar = (ProgressBar)findViewById(R.id.progress_bar);
button.setOnClickListener(this);
}
@Override
public void onClick(View v) {
switch (v.getId()) {
case R.id.button:
//按钮显示进度条
if(progressBar.getVisibility() == View.GONE) {
progressBar.setVisibility(View.VISIBLE);
}else {
progressBar.setVisibility(View.GONE);
}
break;
default:
break;
}
}
}
在按钮的点击事件中,我们通过getVisibility()
方法来判断ProgressBar是否可见,如果可见就将ProgressBar 隐藏掉,如果不可见就将ProgressBar显示出来。重新运行程序,然后不断地点击按钮,你就会看到进度条会在显示与隐藏之间来回切换。
另外,我们还可以给ProgressBar 指定不同的样式,刚刚是圆形进度条,通过style属性可以将它指定成水平进度条,修改activity_ main.xml 中的代码,如下所示:
<ProgressBar
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:id="@+id/progress_bar"
style="?android:attr/progressBarStyleHorizontal"
android:max="100"
/>
指定成水平进度条后,我们还可以通过android:max属性给进度条设置一个最大值,然后在代码中动态地更改进度条的进度。修改MainActivity中的代码,如下所示:
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 = (Button) findViewById(R.id.button);
//获取EditText实例
editText = (EditText) findViewById(R.id.edit_text);
//获取imageView实例
imageView = (ImageView) findViewById(R.id.image_view);
//获取progressBar实例
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 作为更新后的进度。重新运行程序,
AlertDialog
AlertDialog可以在当前的界面弹出一个对话框,这个对话框是置项于所有界面元素之上的,能够屏蔽掉其他控件的交互能力,因此AlertDialog 一般都是用于提示- -些非 常重要的内容或者警告信息。比如为了防止用户误删重要内容,在删除前弹出一一个确认对话框。
MainActivity
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 = (Button) findViewById(R.id.button);
//获取EditText实例
editText = (EditText) findViewById(R.id.edit_text);
//获取imageView实例
imageView = (ImageView) findViewById(R.id.image_view);
//获取progressBar实例
progressBar = (ProgressBar)findViewById(R.id.progress_bar);
button.setOnClickListener(this);
}
@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);
dialog.setPositiveButton("OK", new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialogInterface, int i) {
}
});
dialog.setNegativeButton("Camcel", new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialogInterface, int i) {
}
});
dialog.show();
break;
default:
break;
}
}
}
首先通过AlertDialog Builder创建一个AlertDialog的实例,然后可以为这个对话框设置标题、内容、可否取消等属性,接下来调用setPositiveButton()
方法为对话框设置确定按钮的点击事件,调用setNegativeButton()
方法设置取消按钮的点击事件,最后调用show()
方法将对话框显示出来。
ProgressDialog
ProgressDialog和AlertDialog 有点类似,都可以在界面上弹出一个对话框,都能够屏蔽掉其他控件的交互能力。不同的是,ProgressDialog 会在对话框中显示一个进度条,一般用于表示当前操作比较耗时,让用户耐心地等待。
MainActivity
@Override
public void onClick(View v) {
switch (v.getId()) {
case R.id.button:
//弹出对话框内有进度条
ProgressDialog progressDialog = new ProgressDialog(MainActivity.this);
progressDialog.setTitle("This is ProgressDialog");
progressDialog.setMessage("Loading...");
progressDialog.setCancelable(true);
progressDialog.show();
break;
default:
break;
}
}
可以看到,这里也是先构建出一个ProgressDialog对象,然后同样可以设置标题、内容、可否取消等属性,最后也是通过调用show( )
方法将ProgressDialog显示出来。
注意,如果在
setCancelable()
中传人了false, 表示ProgressDialog 是不能通过Back键取消掉的,这时你就一定要在代码中做好控制,当数据加载完成后必须要调用ProgressDialog的dismiss()
方法来关闭对话框,否则ProgressDialog将会一直存在。
详解四种基本布局
布局是一种可用于放置很多控件的容器,它可以按照一定的规律调整内部控件的位置,从而编写出精美的界面。当然,布局的内部除了放置控件外,也可以放置布局,通过多层布局的嵌套,我们就能够完成一些比较复杂的界面实现。
线性布局
LinearLayout又称作线性布局,是一种非常常用的布局。正如它的名字所描述的一样,这个布局会将它所包含的控件在线性方向上依次排列。
activity_main_xml
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:orientation="vertical"
android:layout_width="match_parent"
android:layout_height="match_parent">
<Button
android:id="@+id/button1"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Button 1"/>
<Button
android:id="@+id/button2"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Button 2"/>
<Button
android:id="@+id/button3"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Button 3"/>
</LinearLayout>
我们在LinearLayout中添加了3个Button,每个Button的长和宽都是wrap_ content, 并指定了排列方向是vertical。现在运行一下程序,
修改排列方式
android:orientation="horizontal"
如果不指定,默认横向
这里需要注意,如果LinearLayout的排列方向是horizontal,内部的控件就绝对不能将宽度指定为match_ parent,因为这样的话,单独一个控件就会将整个水平方向占满,其他的控件就没有可放置的位置了。同样的道理,如果LinearLayout的排列方向是vertical,内部的控件就不能将高度指定为match_parent。
首先来看android:layout_ gravity 属性,它和我们上一节中学到的android:gravity属性看起来有些相似,其实从名字就可以看出,android:gravity用于指定文字在控件中的对齐方式,而android:layout_ gravity 用于指定控件在布局中的对齐方式。android:layout_ gravity 的可选值和android:gravity差不多,但是需要注意,当LinearL ,ayout的排列方向是horizontal时,只有垂直方向上的对齐方式才会生效,因为此时水平方向上的长度是不固定的,每添加一个控件,水平方向上的长度都会改变,因而无法指定该方向上的对齐方式。同样的道理,当LinearLayout 的排列方向是vertical 时,只有水平方向上的对齐方式才会生效。
activity_main.xml
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:orientation="horizontal"
android:layout_width="match_parent"
android:layout_height="match_parent">
<Button
android:id="@+id/button1"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="top"
android:text="Button 1"/>
<Button
android:id="@+id/button2"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center_vertical"
android:text="Button 2"/>
<Button
android:id="@+id/button3"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="bottom"
android:text="Button 3"/>
</LinearLayout>
由于目前LinearLayout的排列方向是horizontal,因此我们只能指定垂直方向上的排列方向,将第个Button的对齐方式指定为top,第二个Button的对齐方式指定为center_ vertical, 第三个Button的对齐方式指定为bottom。
接下来我们学习下LinearLayout 中的另一个重要属性一android:layout_ weight。 这个属性允许我们使用比例的方式来指定控件的大小,它在手机屏幕的适配性方面可以起到非常重要的作用。比如我们正在编写一个消息发送界面,需要一个文本编辑框和一个发送按钮
activity_ main.xml
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:orientation="horizontal"
android:layout_width="match_parent"
android:layout_height="match_parent">
<EditText
android:id="@+id/input_message"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:hint="Type somethinf"/>
<Button
android:id="@+id/send"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:text="Send"/>
</LinearLayout>
dp是Android中用于指定控件大小、间距等属性的单位,在EditText 和Button 里都将android: layout_ weight
属性的值指定为1, 这表示EditText和Button将在水平方向平分宽度。
系统会先把LinearLayout下所有控件指定的layout_ weight
值相加,得到一个总值,然后每个控件所占大小的比例就是用该控件的layout_ weight 值除以刚才算出的总值。因此如果想让EditText占据屏幕宽度的3/5, Button 占据屏幕宽度的2/5,只需要将EditText的layout_weight改成3,Button 的layout_ weight 改成2就可以了。
我们还可以通过指定部分控件的layout_ weight 值来实现更好的效果。
activity_ main.xml
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:orientation="horizontal"
android:layout_width="match_parent"
android:layout_height="match_parent">
<EditText
android:id="@+id/input_message"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="2"
android:hint="Type somethinf"/>
<Button
android:id="@+id/send"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Send"/>
</LinearLayout>
这里我们仅指定了EditText 的android:layout_ weight
属性,并将Button 的宽度改回wrap_ content
。 这表示Button的宽度仍然按照wrap_ content
来计算,而EditText则会占满屏幕所有的剩余空间。使用这种方式编写的界面,不仅在各种屏幕的适配方面会非常好,而且看起来也更加舒服。
相对布局
RelativeL ayout又称作相对布局,也是一种非常常用的布局。和LinearL ayout的排列规则不同,RelativeLayout 显得更加随意些,它可以通过相对定位的方式让控件出现在布局的任何位置。
activity_ main.xml
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent">
<Button
android:id="@+id/button1"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentLeft="true"
android:layout_alignParentTop="true"
android:text="Button 1"/>
<Button
android:id="@+id/button2"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentRight="true"
android:layout_alignParentTop="true"
android:text="Button 2"/>
<Button
android:id="@+id/button3"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_centerInParent="true"
android:text="Button 3"/>
<Button
android:id="@+id/button4"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentLeft="true"
android:layout_alignParentBottom="true"
android:text="Button 4"/>
<Button
android:id="@+id/button5"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentRight="true"
android:layout_alignParentBottom="true"
android:text="Button 5"/>
</RelativeLayout>
上面每个例子都是相当于父布局定位的 下面是相对于控件进行定位,相对于按钮3进行定位,
当一个控件去引用另一个控件的id时,该控件一定要定义在引用控件的后面,不然会出现找不到id的情况
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent">
<Button
android:id="@+id/button3"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_centerInParent="true"
android:text="Button 3"/>
<Button
android:id="@+id/button1"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_above="@+id/button3"
android:layout_toLeftOf="@+id/button3"
android:text="Button 1"/>
<Button
android:id="@+id/button2"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_above="@+id/button3"
android:layout_toRightOf="@+id/button3"
android:text="Button 2"/>
<Button
android:id="@+id/button4"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_below="@+id/button3"
android:layout_toLeftOf="@+id/button3"
android:text="Button 4"/>
<Button
android:id="@+id/button5"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_below="@+id/button3"
android:layout_toRightOf="@+id/button3"
android:text="Button 5"/>
</RelativeLayout>
RelativeLayout中还有另外一组相对于控件进行定位的属性,android:layout_ alignLeft
表示让一个控件的左边缘和另-一个控件的左边缘对齐, android: layout_ _alignRight
表示让一个控件的右边缘和另一个控件的右边缘对齐。此外,还有android:layout_ _alignTop
和android: layout_ alignBottom
, 道理都是一样的,
帧布局
FrameLayout又称作帧布局,它相比于前面两种布局就简单太多了,因此它的应用场景也少了很多。这种布局没有方便的定位方式,所有的控件都会默认摆放在布局的左上角。
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent">
<TextView
android:id="@+id/text_view"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="This is TextView"/>
<ImageView
android:id="@+id/image_view"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:src="@mipmap/ic_launcher"/>
</FrameLayout>
文字和图片都是位于布局的左上角。由于ImageView是在TextView之后添加的,因此图片压在了文字的上面。
我们也可以通过android:layout_gravity
来指定布局的对齐方式
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent">
<TextView
android:id="@+id/text_view"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="left"
android:text="This is TextView"/>
<ImageView
android:id="@+id/image_view"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="right"
android:src="@mipmap/ic_launcher"/>
</FrameLayout>
约束布局
ConstraintLayout和传统编写界面的方式恰恰相反,ConstraintLayout非常适合使用可视化的方式来编写界面,但并不太适合使用XML的方式来进行编写。当然,可视化操作的背后仍然还是使用的XML代码来实现的,只不过这些代码是由Android Studio根据我们的操作自动生成的。
另外,ConstraintLayout还有一个优点,它可以有效地解决布局嵌套过多的问题。我们平时编写界面,复杂的布局总会伴随着多层的嵌套,而嵌套越多,程序的性能也就越差。ConstraintLayout则是使用约束的方式来指定各个控件的位置和关系的,它有点类似于RelativeLayout,但远比RelativeLayout要更强大。
自定义控件
可以看到,我们所用的所有控件都是直接或间接继承自View的,所用的所有布局都是直接或间接继承自ViewGroup 的。View 是Android中最基本的一-种UI组件,它可以在屏幕上绘制一块矩形区域,并能响应这块区域的各种事件,因此,我们使用的各种控件其实就是在View的基础之_上又添加了各自特有的功能。而ViewGroup则是一-种特殊的View,它可以包含很多子View 和子ViewGroup,是一个用于放置控件和布局的容器。
引入布局
title.xml
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="@drawable/edit_bg">
<Button
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:id="@+id/title_back"
android:layout_gravity="center"
android:layout_margin="5dp"
android:background="@drawable/back_bg"
android:text="Back"
android:textColor="#fff"/>
<TextView
android:layout_width="0dp"
android:layout_height="wrap_content"
android:id="@+id/title_text"
android:layout_gravity="center"
android:layout_weight="1"
android:gravity="center"
android:text="Title Text"
android:textColor="#fff"
android:textSize="24sp"/>
<Button
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:id="@+id/title_edit"
android:layout_gravity="center"
android:layout_margin="5dp"
android:background="@drawable/title_bg"
android:text="Edit"
android:textColor="#fff"/>
</LinearLayout>
android:background用于为布局或控件指定一个背景,可以使用颜色或图片进行填充
在程序中使用这个标题栏
<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>
MainActivity
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();
}
}
}
使用这种方式,不管有多少布局需要添加标题栏,只需一行include语句就可以了。
创建自定义控件
TitleLayout
public class TitleLayout extends LinearLayout {
public TitleLayout(Context context, 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
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent">
<com.example.uilayouttest.TitleLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"/>
</LinearLayout>
添加自定义控件和添加普通控件的方式基本是一样的,只不过在添加自定义控件的时候,我们需要指明控件的完整类名,包名在这里是不可以省略的。
这与引用布局效果一样
为标题栏中的按钮注册点击事件,修改TitleLayout中的代码
public class TitleLayout extends LinearLayout {
public TitleLayout(Context context, AttributeSet attrs) {
super(context, attrs);
LayoutInflater.from(context).inflate(R.layout.title, this);
Button titleBacke = (Button) findViewById(R.id.title_back);
Button titleEdit = (Button) findViewById(R.id.title_edit);
titleBacke.setOnClickListener(new OnClickListener() {
@Override
public void onClick(View view) {
((Activity) getContext()).finish();
}
});
titleEdit.setOnClickListener(new OnClickListener() {
@Override
public void onClick(View view) {
Toast.makeText(getContext(), "You clicked Edit button", Toast.LENGTH_SHORT).show();
}
});
}
}
首先还是通过findViewById()方法得到按钮的实例,然后分别调用setOnClickListener()方法给两个按钮注册了点击事件,当点击返回按钮时销毁掉当前的活动,当点击编辑按钮时弹出一段文本。
这样的话,每当我们在一一个布局中引入TitleLayout 时,返回按钮和编辑按钮的点击事件就已经自动实现好了,这就省去了很多编写重复代码的工作。
ListView
ListView绝对可以称得上是Android中最常用的控件之一,几乎所有的应用程序都会用到它。由于手机屏幕空间都比较有限,能够一次性在屏幕上显示的内容并不多,当我们的程序中有大量的数据需要展示的时候,就可以借助ListView来实现。ListView允许用户通过手指上下滑动的方式将屏幕外的数据滚动到屏幕内,同时屏幕上原有的数据则会滚动出屏幕。相信你其实每天都在使用这个控件,比如查看QQ聊天记录,翻阅微博最新消息,等等。
ListView的简单用法
新建一个ListViewTest项目,修改activity_ maln.xml中的代码
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent">
<ListView
android:layout_width="match_parent"
android:layout_height="match_parent"
android:id="@+id/list_view"/>
</LinearLayout>
接下来修改MainActivity中的代码,如下所示:
public class MainActivity extends AppCompatActivity {
private String[] data = {"Apple", "Banana", "Orange", "Watermelon", "Pear", "Grape", "Pineapple", "Strawberry", "Cherry", "Mango", "Apple", "Banana", "Orange", "Watermelon", "Pear", "Grape", "Pineapple", "Strawberry", "Cherry", "Mango"};
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
ArrayAdapter<String> adapter = new ArrayAdapter<String>(MainActivity.this, android.R.layout.simple_list_item_1, data);
ListView listView = (ListView) findViewById(R.id.list_view);
listView.setAdapter(adapter);
}
}
不过,数组中的数据是无法直接传递给ListView 的,我们还需要借助适配器来完成。Android中提供了很多适配器的实现类,其中我认为最好用的就是ArrayAdapter。它可以通过泛型来指定要适配的数据类型,然后在构造函数中把要适配的数据传人。ArrayAdapter 有多个构造函数的重载,这里由于我们提供的数据都是字符串,因此将ArrayAdapter的泛型指定为String,然后在ArrayAdapter 的构造函数中依次传人当前上下文、ListView子项布局的id,以及要适配的数据。注意,我们使用了android. R. layout. simple_list_ item_ 1作为ListView子项布局的id, 这是一个Android内置的布局文件,里面只有一个TextView,可用于简单地显示一段文本。 这样适配器对象就构建好了。
最后,还需要调用ListView的setAdapter( )
方法,将构建好的适配器对象传递进去,这样ListView和数据之间的关联就建立完成了。
定制ListView的界面
显示文本与图片,首先定义一个实体类,作为ListView适配器的适配类型。
public class Fruit {
private String name;
private int imageId;
public Fruit(String name, int imageId) {
this.name = name;
this.imageId = imageId;
}
public String getName() {
return name;
}
public int getImageId() {
return imageId;
}
}
然后需要为ListView的子项指定一个我们自定义的布局,在layout目录下新建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:layout_width="wrap_content"
android:layout_height="wrap_content"
android:id="@+id/fruit_image"/>
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:id="@+id/fruit_name"
android:layout_gravity="center_vertical"
android:layout_marginLeft="10dp"/>
</LinearLayout>
在这个布局中,我们定义了一个ImageView用于显示水果的图片,又定义了一个TextView用于显示水果的名称,并让TextView在垂直方向上居中显示。
接下来需要创建-一个自定义的适配器,这个适配器继承自ArrayAdapter, 并将泛型指定为Fruit类。新建类FruitAdapter,
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中了。
我们继续往下看,接下来调用View 的findViewById( )
方法分别获取到ImageView 和TextView的实例,并分别调用它们的setImageResource( )
和setText()
方法来设置显示的图片和文字,最后将布局返回,这样我们自定义的适配器就完成了。
修改MainActivity
public class MainActivity extends AppCompatActivity {
//private String[] data = {"Apple", "Banana", "Orange", "Watermelon", "Pear", "Grape", "Pineapple", "Strawberry", "Cherry", "Mango", "Apple", "Banana", "Orange", "Watermelon", "Pear", "Grape", "Pineapple", "Strawberry", "Cherry", "Mango"};
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显示水果名称
//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);
}
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 orange = new Fruit("Orange", R.drawable.orange_pic);
fruitList.add(orange);
Fruit watermelon = new Fruit("Watermelon", R.drawable.watermelon_pic);
fruitList.add(watermelon);
Fruit pear = new Fruit("Pear", R.drawable.pear_pic);
fruitList.add(pear);
Fruit grape = new Fruit("Grape", R.drawable.grape_pic);
fruitList.add(grape);
Fruit pineapple = new Fruit("Pineapple", R.drawable.pineapple_pic);
fruitList.add(pineapple);
Fruit strawberry = new Fruit("Strawberry", R.drawable.strawberry_pic);
fruitList.add(strawberry);
Fruit cherry = new Fruit("Cherry", R.drawable.cherry_pic);
fruitList.add(cherry);
Fruit peach = new Fruit("Apple", R.drawable.peach_pic);
fruitList.add(peach);
}
}
}
可以看到,这里添加了一个initFruits()
方法,用于初始化所有的水果数据。在Fruit类的构造函数中将水果的名字和对应的图片id传入,然后把创建好的对象添加到水果列表中。另外我们使用了一个for循环将所有的水果数据添加了两遍,这是因为如果只添加一遍的话,数据量还不足以充满整个屏幕。接着在onCreate()
方法中创建了FruitAdapter对象,并将Fruit-Adapter作为适配器传递给ListView,这样定制ListView 界面的任务就完成了。
提升ListView的运行效率
目前我们的ListView运行效率较低,因为在FruitAdapter的getView()方法中,每次都将布局重新加载一遍,当快速滚动时,这就成为了性能瓶颈。getView()方法中还有一个convertView参数,这个参数用于将之前加载好的布局进行缓存,以便之后可以进行重用。
FruitAdapter
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);
View view;
if(convertView == null) {
view = LayoutInflater.from(getContext()).inflate(resourceId, parent, false);
}else {
view = convertView;
}
//获取当前项的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;
}
}
增加if判断语句 可以大大提高效率,展现出更好的性能。
优化每次在getView()方法中调用View的findViewById()方法来获取一次控件的实例,借助ViewHolder来对这部分性能进行优化。
FruitAdapter
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);
View view;
ViewHolder viewHolder ;
if(convertView == null) {
//获取当前项的Fruit实例
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的点击事件
MainActivity.java
@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显示水果名称
//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.setOnItemClickListener(new AdapterView.OnItemClickListener() {
@Override
public void onItemClick(AdapterView<?> adapterView, View view, int position, long l) {
Fruit fruit = fruitList.get(position);
Toast.makeText(MainActivity.this, fruit.getName(), Toast.LENGTH_SHORT).show();
}
});
}
可以看到,我们使用setOnItemClickListener()
方法为ListView注册了一个监听器,当用户点击了ListView中的任何一个子项时,就会回调onItemClick()
方法。 在这个方法中可以通过position参数判断出用户点击的是哪一个子项,然后获取到相应的水果,并通过Toast将水果的名字显示出来。
RecyclerView
ListView 的扩展性也不够好,它只能实现数据纵向滚动的效果,如果我们想实现横向滚动的话,ListView 是做不到的。
为此,Android 提供了一个更强大的滚动控件——RecyclerView。
它可以说是一个增强版的ListView,不仅可以轻松实现和ListView同样的效果,还优化了ListView 中存在的各种不足之处。
首先新建-一个RecyclerViewTest项目,并让Android Studio自动帮我们创建好活动。
RecyclerView的基本用法
RecyclerView属于新增的控件,为了让RecyclerView在所有Android版本上都能使用,Android 团队将RecyclerView 定义在了support 库当中。因此,想要使用RecyclerView这个控件,首先需要在项目的build.gradle中添加相应的依赖库才行。
打开app/build.gradle文件,在dependencies闭包中添加如下内容:
implementation 'androidx.recyclerview:recyclerview:1.0.0'
添加完之后记得要点击一下Sync Now来进行同步。然后修改activity_ main.xml 中的代码,
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent">
<androidx.recyclerview.widget.RecyclerView
android:layout_width="match_parent"
android:layout_height="match_parent"
android:id="@+id/recycler_view"/>
</LinearLayout>
在布局中加入RecyclerView 控件也是非常简单的,先为RecyclerView 指定一个id,然后将宽度和高度都设置为match_ parent, 这样RecyclerView也就占满了整个布局的空间。需要注意的是,由于RecyclerView并不是内置在系统SDK当中的,所以需要把完整的包路径写出来。
创建与ListViewTest项目相同的Fruit类与fruit_item.xml
接下来需要为RecyclerView准备一个适配器,新建FruitAdapter类,让这个适配器继承自RecyclerView.Adapter, 并将泛型指定为FruitAdapter.ViewHolder。 其中,ViewHolder是我们在FruitAdapter中定义的一个内部类,代码如下所示:
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;
}
@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;
}
@Override
public void onBindViewHolder(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 要继承自RecyclerView. ViewHolder。然后:ViewHolder的构造函数中要传入-一个View参数,这个参数通常就是RecyclerView子项的最外层布局,那么我们就可以通过findViewById()方法来获取到布局中的ImageView和TextView的实例了。
FruitAdapter中也有一个构造函数,这个方法用于把要展示的数据源传进来,并赋值给一 个全局变量mFruitList,我们后续的操作都将在这个数据源的基础上进行。
继续往下看,由于FruitAdapter是继承自RecyclerView. Adapter的,那么就必须重写onCreateViewHolder()、onBindViewHolder()和 getItemCount()这 3个方法。onCreate-ViewHolder( )方法是用于创建ViewHolder实例的,我们在这个方法中将fruit_ item 布局加载进来,然后创建-一个ViewHolder实例,并把加载出来的布局传人到构造函数当中,最后将ViewHolder的实例返回。onBindVi ewHolder()方法是用于对RecyclerView子项的数据进行赋值的,会在每个子项被滚动到屏幕内的时候执行,这里我们通过position参数得到当前项的Fruit实例,然后再将数据设置到ViewHolder的ImageView和TextView当中即可。getItemCount()方法就非常简单了,它用于告诉RecyclerView-共有多少子项,直接返回数据源的长度就可以了。
适配器准备好了之后,我们就可以开始使用RecyclerView了
MainActivity
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 orange = new Fruit("Orange", R.drawable.orange_pic);
fruitList.add(orange);
Fruit watermelon = new Fruit("Watermelon", R.drawable.watermelon_pic);
fruitList.add(watermelon);
Fruit pear = new Fruit("Pear", R.drawable.pear_pic);
fruitList.add(pear);
Fruit grape = new Fruit("Grape", R.drawable.grape_pic);
fruitList.add(grape);
Fruit pineapple = new Fruit("Pineapple", R.drawable.pineapple_pic);
fruitList.add(pineapple);
Fruit strawberry = new Fruit("Strawberry", R.drawable.strawberry_pic);
fruitList.add(strawberry);
Fruit cherry = new Fruit("Cherry", R.drawable.cherry_pic);
fruitList.add(cherry);
Fruit peach = new Fruit("Apple", R.drawable.peach_pic);
fruitList.add(peach);
}
}
}
可以看到,这里使用了一个同样的initFruits()
方法,用于初始化所有的水果数据。接着在onCreate( )
方法中我们先获取到RecyclerView 的实例,然后创建了一个LinearLayout -Manager对象,并将它设置到RecyclerView 当中。LayoutManager 用于指定RecyclerView的布局方式,这里使用LinearLayoutManager是线性布局的意思,可以实现和ListView类似的效果。接下来我们创建了FruitAdapter的实例,并将水果数据传人到FruitAdapter的构造函数中,最后调用RecyclerView的setAdapter()
方法来完成适配器设置,这样RecyclerView和数据之间的关联就建立完成了。
实现横向滚动和瀑布流布局
实现横向滚动,首先要对fruit_item布局修改,因为目前这个布局里面的元素是水平排列的,适用于纵向滚动的场景,而如果我们要实现横向滚动的话,应该把fruit_ item里的元素改成垂直排列才比较合理。
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:orientation="vertical"
android:layout_width="100dp"
android:layout_height="wrap_content">
<ImageView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:id="@+id/fruit_image"
android:layout_gravity="center_horizontal"/>
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:id="@+id/fruit_name"
android:layout_gravity="center_vertical"
android:layout_marginLeft="10dp"/>
</LinearLayout>
可以看到,我们将LinearLayout改成垂直方向排列,并把宽度设为100dp。这里将宽度指定
为固定值是因为每种水果的文字长度不一致,如果用wrap_content 的话,RecyclerView的子项就会有长有短,非常不美观;而如果用match_parent 的话,就会导致宽度过长,一个子项占满整个屏幕。
然后我们将ImageView 和TextView 都设置成了在布局中水平居中,并且使用layout_marginTop属性让文字和图片之间保持一些距离 。
接下来修改MainActivity中的代码,如下所示:
@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);
layoutManager.setOrientation(LinearLayoutManager.HORIZONTAL);
recyclerView.setLayoutManager(layoutManager);
FruitAdapter adapter = new FruitAdapter(fruitList);
recyclerView.setAdapter(adapter);
}
MainActivity中只加入了一行代码,调用LinearLayoutManager的setOrientation()
方法来设置布局的排列方向,默认是纵向排列的,我们传人LinearLayoutManager . HORIZONTAL表示让布局横行排列,这样RecyclerView就可以横向滚动了。
ListView的布局排列是由自身去管理的,而RecyclerView则将这个工作交给了LayoutManager, LayoutManager 中制定了一套可扩展的布局排列接口,子类只要按照接口的规范来实现,就能定制出各种不同排列方式的布局了。
除了LinearLayoutManager 之外,RecyclerView 还给我们提供了GridLayoutManager 和StaggeredGridL ayoutManager这两种内置的布局排列方式。GridLayoutManager可以用于实现网格布局,StaggeredGridL ayoutManager可以用于实现瀑布流布局。这里我们来实现一下瀑布流布局。
首先还是来修改一下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:layout_width="wrap_content"
android:layout_height="wrap_content"
android:id="@+id/fruit_image"
android:layout_gravity="center_horizontal"/>
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:id="@+id/fruit_name"
android:layout_gravity="left"
android:layout_marginLeft="10dp"/>
</LinearLayout>
这里做了几处小的调整,首先将LinearLayout的宽度由100dp 改成了match_ parent, 因为瀑布流布局的宽度应该是根据布局的列数来自动适配的,而不是一个固定值。 另外我们使用了layout_ margin 属性来让子项之间互留一点间距,这样就不至于所有子项都紧贴在一些。还有就是将TextView的对齐属性改成了居左对齐,因为待会我们会将文字的长度变长,如果还是居中显示就会感觉怪怪的。
接着修改MainActivity中的代码,如下所示:
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);
//layoutManager.setOrientation(LinearLayoutManager.HORIZONTAL);
//使布局可以横向排列
StaggeredGridLayoutManager layoutManager = new StaggeredGridLayoutManager(3, StaggeredGridLayoutManager.VERTICAL);
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 orange = new Fruit(getRandomLengthName("Orange"), R.drawable.orange_pic);
fruitList.add(orange);
Fruit watermelon = new Fruit(getRandomLengthName("Watermelon"), R.drawable.watermelon_pic);
fruitList.add(watermelon);
Fruit pear = new Fruit(getRandomLengthName("Pear"), R.drawable.pear_pic);
fruitList.add(pear);
Fruit grape = new Fruit(getRandomLengthName("Grape"), R.drawable.grape_pic);
fruitList.add(grape);
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);
Fruit cherry = new Fruit(getRandomLengthName("Cherry"), R.drawable.cherry_pic);
fruitList.add(cherry);
Fruit peach = new Fruit(getRandomLengthName("Peach"), R.drawable.peach_pic);
fruitList.add(peach);
}
}
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()方法中,我们创建了一个StaggeredGridL ayoutManager的实例。StaggeredGridLayoutManager的构造函数接收两个参数,第一个参数用于指定布局的列数,传人3表示会把布局分为3列;第二个参数用于指定布局的排列方向,传人StaggeredGrid-LayoutManager . VERTICAL表示会让布局纵向排列,最后再把创建好的实例设置到RecyclerView当中就可以了。
由于瀑布流布局需要各个子项的高度不一致才能看出明显的效果,为此我又使用了一个小技巧。getRandomLengthName()这个方法使用了Random 对象来创造一个 1到20之间的随机数,然后将参数中传人的字符串重复随机遍。在initFruits()方法中,每个水果的名字都改成调用getRandomLengthName()这个方法来生成,这样就能保证各水果名字的长短差距较大,子项的高度也就不同了
RecyclerView的点击事件
和ListView一样,RecyclerView也必须要能影响点击事件才可以,不然的话就没什么实际用途了。不过不同于ListView的是,RecyclerView 并没有提供类似于setOnItemClickListener()
这样的注册监听器方法,而是需要我们自己给子项具体的View去注册点击事件,相比于ListView来说,实现起来要复杂些。
ListView 在点击事件上的处理并不人性化,setOnItemClickListener()
方法注册的是子项的点击事件,但如果我想点击的是子项里具体的某一个按钮,虽然ListView也是能做到的,但是实现起来就相对比较麻烦了。
下面我们来具体学习一下如何在RecyclerView中注册点击事件,修改FruitAdapter中的代码,如下所示:
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;
}
@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);
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(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变量来保存子项最外层布局的实例,然后在onCreateVi ewHolder( )方法中注册点击事件就可以了。这里分别为最外层布局和ImageView都注册了点击事件,RecyclerView 的强大之处也在这里,它可以轻松实现子项中任意控件或布局的点击事件。我们在两个点击事件中先获取了用户点击的position, 然后通过position拿到相应的Fruit实例,再使用Toast分别弹出两种不同的内容以示区别。
编写界面的最佳实例
制作Nine-Patch图片
Nine-Patch它是一种被特殊处理过的png图片,能够指定哪些区域可以被拉伸、哪些区域不可以。
比如说项目中有一张气泡样式的图片message_ left.png。
我们将这张图片设置为LinearLayout 的背景图片,修改activity_ main.xml 中的代码,如下
<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的宽度不足以填满整个屏幕的宽度,整张图片被均匀地拉伸了。接下来我们需要改善这种情况。
我们就是使用draw9patch.bat来制作Nine-Patch图片的。不过,要打开这个文件,必须先将JDK的bin目录配置到环境变量当中才行,比如你使用的是Android Studio内置的jdk,那么要配置的路径就是<AndroidStudio安装目录>/jre/bin。选中你需要修改的图片,右击选择Create 9-Patch file,
我们可以在图片的四个边框绘制一个个的小黑点,在上边框和左边框绘制的部分表示当图片需要拉伸时就拉伸黑点标记的区域,在下边框和右边框绘制的部分表示内容会被放置的区域。使用鼠标在图片的边缘拖动就可以进行绘制了,按住Shift 键拖动可以进行擦除。
修改好照片后将原照片删除。替换照片后重新运行程序
这样当图片需要拉伸的时候,就可以只拉伸指定的区域,程序在外观上也有了很大的改进。
编写精美的聊天界面
制作一 张message_right.9.png作为发出消息的背景图。图片都提供好了之后就可以开始编码了。由于待会我们会用到RecyclerView,因此首先需要在app/build.gradle当中添加依赖库。
implementation 'androidx.recyclerview:recyclerview:1.0.0'
修改activity_ main.xml 中的代码,
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:orientation="vertical"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="#d8e0e8">
<androidx.recyclerview.widget.RecyclerView
android:layout_width="match_parent"
android:layout_height="0dp"
android:id="@+id/msg_recycler_view"
android:layout_weight="1"/>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content">
<EditText
android:layout_width="0dp"
android:layout_height="wrap_content"
android:id="@+id/input_text"
android:layout_weight="1"
android:hint="Type something here"
android:maxLines="2"/>
<Button
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:id="@+id/send"
android:text="Send"/>
</LinearLayout>
</LinearLayout>
我们在主界面中放置了一个RecyclerView用于显示聊天的消息内容,又放置了一个EditText用于输人消息,还放置了一个Button用于发送消息。
然后定义消息的实体类,新建Msg,代码如下所示:
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, 代码如下所示:
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:orientation="vertical"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:padding="10dp">
<LinearLayout
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:id="@+id/left_layout"
android:layout_gravity="left"
android:background="@drawable/message_left">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:id="@+id/left_msg"
android:layout_gravity="center"
android:layout_margin="10dp"
android:textColor="#fff"/>
</LinearLayout>
<LinearLayout
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:id="@+id/right_layout"
android:layout_gravity="right"
android:background="@drawable/message_right">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:id="@+id/right_msg"
android:layout_gravity="center"
android:layout_margin="10dp"/>
</LinearLayout>
</LinearLayout>
这里我们让收到的消息居左对齐,发出的消息居右对齐,并且分别使用message ef.9.png 和mesage right.9.png 作为背景图。
接下来需要创建RecyclerView 的适配器类,新建类MsgAdapter,代码如下所示:
public class MsgAdapter extends RecyclerView.Adapter<MsgAdapter.ViewHolder> {
private List<Msg> mMsgList;
static class ViewHolder extends RecyclerView.ViewHolder {
LinearLayout leftLayout;
LinearLayout rightLayout;
TextView leftMsg;
TextView 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);
}
}
public MsgAdapter(List<Msg> msgList) {
mMsgList = msgList;
}
@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.rightLayout.setVisibility(View.VISIBLE);
holder.leftLayout.setVisibility(View.GONE);
holder.rightMsg.setText(msg.getContent());
}
}
@Override
public int getItemCount() {
return mMsgList.size();
}
}
在onBindViewHolder()方法中增加了对消息类型的判断。如果这条消息是收到的,则显示左边的消息布局,如果这条消息是发出的,则显示右边的消息布局。
最后修改MainActivity中的代码,来为RecyclerView初始化一些数据, 并给发送按钮加入事;件响应,代码如下所示:
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);
//当有新消息时,刷新ListView中的显示
msgRecyclerView.scrollToPosition(msgList.size() - 1);
//将ListView定位到最后一行
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 Tom, Nice Talking to you", Msg.TYPE_RECEIVED);
msgList.add(msg3);
}
}
在initMsgs ( )方法中我们先初始化了几条数据用于在RecyclerView中显示。然后在发送按钮的点击事件里获取了EditText 中的内容,如果内容不为null则创建出一个新的Msg对象,并把它添加到msgList列表中去。之后又调用了适配器的notifyI temInserted()方法,用于通知列表有新的数据插人,这样新增的一-条消息才能够在RecyclerView 中显示。接着调用RecyclerView的scrollToPosition( )方法将显示的数据定位到最后一行,以保证一定可以看得到最后发出的一条消息。最后调用EditText的setText()方法将输人的内容清空。