第三章 UI开发
3.2 常用的控件
3.2.1 TextView
- android:gravity指文字的对齐方式:top/bottom/left/right/center
- android:textSize="24sp" android中字体大小以sp为单位
3.2.2 Button
Button点击事件的注册模式有两种,一是以匿名类方式,二是以继承接口的方式.
匿名类方式示例如下:
public class FirstActivity extends BaseActivity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.first_layout);
Button button1 = findViewById(R.id.button1);
button1.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View view) {
// 业务逻辑
}
});
}
}
继承接口方式示例:
public class ThirdActivity extends BaseActivity implements View.OnClickListener {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.third_layout);
Button button = findViewById(R.id.button3);
button.setOnClickListener(this);
}
@Override
public void onClick(View view) {
switch (view.getId()){
case R.id.button3:
ActivityCollector.finishAll();
Log.d("ThirdActivity", String.format("Process exit %s", android.os.Process.myPid()));
android.os.Process.killProcess(android.os.Process.myPid());
break;
default:
break;
}
}
}
3.2.3 EditText
- android:hint 可以给EditText一个提示性的文本
- android:maxLines 指定了EditText的最大行数为两行,超过两行后文本自动向上滚动而不会在继续拉伸
增加一个button来获取EditText输入的内容,button使用继承的方式来实现监听.新建项目UIWidgetTest,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 = findViewById(R.id.button);
button.setOnClickListener(this);
editText = findViewById(R.id.edit_text);
}
@Override
public void onClick(View view) {
switch (view.getId()){
case R.id.button:
String inputText = editText.getText().toString();
Toast.makeText(MainActivity.this,inputText,Toast.LENGTH_SHORT).show();
break;
default:
break;
}
}
}
3.2.4 ImageView
ImageView是一个展示图片的控件,图片一般是放在drawable开头的目录下.默认项目会创建一个空的drawable目录,不过由于这个目录没有指定具体的分辨率,所以一般不使用此目录来放置图片.在res下新建drawable-xhdpi目录,并将准备好的图片复制到目录中.在android视图下不显示新建的目录,可以切换到project视图下.在新建drawable-xhdpi目录时,需要将Available qualifiers属性选择为Density并选择对应的分辨率.
修改activity_main.xml来显示图片,因为未知图片的宽和高,所以设置为wrap_content,保证图片完全显示.代码如下:
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical">
<EditText
android:id="@+id/edit_text"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:hint="Type something"
android:maxLines="2" />
<Button
android:id="@+id/button"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="button"></Button>
<ImageView
android:id="@+id/image_view"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:src="@drawable/img7"></ImageView>
</LinearLayout>
通过程序可以动态的修改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 = findViewById(R.id.button);
editText = findViewById(R.id.edit_text);
imageView = findViewById(R.id.image_view);
button.setOnClickListener(this);
}
@Override
public void onClick(View view) {
switch (view.getId()) {
case R.id.button:
String inputText = editText.getText().toString();
Toast.makeText(MainActivity.this, inputText, Toast.LENGTH_SHORT).show();
imageView.setImageResource(R.drawable.img8);
break;
default:
break;
}
}
}
3.2.5 ProgressBar
progressBar用于显示进度条,基本用法如下:
<ProgressBar
android:id="@+id/progress_bar"
android:layout_width="match_parent"
android:layout_height="wrap_content"/>
默认情况下.进度是可见的.所有的Android控件的都有可见属性,通过android:visibility来指定,可选参数有:visible,invisible,gone.
- visible :可见
- invisable:不可见,但占位置
- gnoe: 不可见,不占位置
在MainActivity中增加一个Button,用来测试修改progressBar的状态.修改后的avtivity_main.xml如下:
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical">
<ProgressBar
android:id="@+id/progress_bar"
android:layout_width="match_parent"
android:layout_height="wrap_content"/>
<Button
android:id="@+id/progress_button"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="change ProgressBar"/>
</LinearLayout>
修改后MainActivity.java如下
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 = findViewById(R.id.button);
editText = findViewById(R.id.edit_text);
imageView = findViewById(R.id.image_view);
button.setOnClickListener(this);
progressBar = findViewById(R.id.progress_bar);
Button processButton = findViewById(R.id.progress_button);
processButton.setOnClickListener(this);
}
@Override
public void onClick(View view) {
switch (view.getId()) {
case R.id.button:
String inputText = editText.getText().toString();
Toast.makeText(MainActivity.this, inputText, Toast.LENGTH_SHORT).show();
imageView.setImageResource(R.drawable.img8);
break;
case R.id.progress_button:
if (progressBar.getVisibility() == View.GONE) {
progressBar.setVisibility(View.VISIBLE);
} else {
progressBar.setVisibility(View.GONE);
}
break;
default:
break;
}
}
}
默认情况下ProgressBar是圆形的,可以修改其样式,比如改成水平进度条
<ProgressBar
android:id="@+id/progress_bar"
android:layout_width="match_parent"
android:layout_height="wrap_content"
style="?android:attr/progressBarStyleHorizontal"
android:max="100"/>
style定义了样式,max属性给进度条设置了一个最大值.修改MainActivity.java,使每点击一次按钮,进度条进度前进一下:
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 = findViewById(R.id.button);
editText = findViewById(R.id.edit_text);
imageView = findViewById(R.id.image_view);
button.setOnClickListener(this);
progressBar = findViewById(R.id.progress_bar);
Button processButton = findViewById(R.id.progress_button);
processButton.setOnClickListener(this);
}
@Override
public void onClick(View view) {
switch (view.getId()) {
case R.id.button:
String inputText = editText.getText().toString();
Toast.makeText(MainActivity.this, inputText, Toast.LENGTH_SHORT).show();
imageView.setImageResource(R.drawable.img8);
break;
case R.id.progress_button:
// 修改可见性
// if (progressBar.getVisibility() == View.GONE) {
// progressBar.setVisibility(View.VISIBLE);
// } else {
// progressBar.setVisibility(View.GONE);
// }
int progress = progressBar.getProgress();
progress = progress+10;
progressBar.setProgress(progress);
break;
default:
break;
}
}
}
3.2.6 AlertDialog
AlertDialog可以在当前的界面弹出一个对话框,这个对话框位置置于所有元素之上,能够屏蔽掉其他控件的交互能力.AlertDialog通过AlertDialog.Builder来实现.setPositiveButton()方法设置对话框确定按钮的事件,setNegativeButton()方法设置取消按钮的事件.修改MainActivity.java如下,点击更新进度的按钮后,弹出提示.dialog.setCancelable(false)表示不能通过Back键取消掉.
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 = findViewById(R.id.button);
editText = findViewById(R.id.edit_text);
imageView = findViewById(R.id.image_view);
button.setOnClickListener(this);
progressBar = findViewById(R.id.progress_bar);
Button processButton = findViewById(R.id.progress_button);
processButton.setOnClickListener(this);
}
@Override
public void onClick(View view) {
switch (view.getId()) {
case R.id.button:
String inputText = editText.getText().toString();
Toast.makeText(MainActivity.this, inputText, Toast.LENGTH_SHORT).show();
imageView.setImageResource(R.drawable.img8);
break;
case R.id.progress_button:
// 修改可见性
// if (progressBar.getVisibility() == View.GONE) {
// progressBar.setVisibility(View.VISIBLE);
// } else {
// progressBar.setVisibility(View.GONE);
// }
// 测试进度
int progress = progressBar.getProgress();
progress = progress+10;
progressBar.setProgress(progress);
// AlertDialog Demo
AlertDialog.Builder dialog = new AlertDialog.Builder(MainActivity.this);
dialog.setTitle(String.format("Title 进度---%s", progress));
dialog.setMessage(String.format("Message 进度---%s", progress));
dialog.setCancelable(false);
dialog.setPositiveButton("ok", new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialogInterface, int i) {
Log.d("MainActivity","It's ok");
}
});
dialog.setNegativeButton("取消", new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialogInterface, int i) {
Log.d("MainActivity", "取消");
}
});
dialog.show();
break;
default:
break;
}
}
}
3.2.7 ProgressDialog
progressDialog和AlertDialog有点类似,都是弹出一个对话框,不同的是,ProgressDialog会在对话框中先是一个进度条,一般用于表示当前操作耗时.另外当数据加载后,必须带调用ProgressDialog的dismiss()方法来关闭对话框.
现在模拟一个ProgressDialog,在打开对话框后向另一个Activity发起一个Intent,并获取其返回的数据,数据返回后,关闭此progressDialog.
新建一个activity起名ProgressDialogBack,用来返回数据给MainAcitivity.代码如下:
public class ProgressDialogBack extends AppCompatActivity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_progress_dialog_back);
Intent intent1 = new Intent();
intent1.putExtra("data_return","data is ok");
setResult(RESULT_OK,intent1);
finish();
}
}
修改activity_main.xml及MainActivity.java,增加一个button并绑定事件,生成一个progressDialog,同时获取返回值并关闭progressDialog.MainActivity.java代码如下activity_main.xml代码略.
public class MainActivity extends AppCompatActivity implements View.OnClickListener {
private EditText editText;
private ImageView imageView;
private ProgressBar progressBar;
private ProgressDialog progressDialog;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
Button button = findViewById(R.id.button);
editText = findViewById(R.id.edit_text);
imageView = findViewById(R.id.image_view);
button.setOnClickListener(this);
progressBar = findViewById(R.id.progress_bar);
Button processButton = findViewById(R.id.progress_button);
processButton.setOnClickListener(this);
progressDialog = new ProgressDialog(MainActivity.this);
Button progress_dialog_button = findViewById(R.id.progress_dialog_button);
progress_dialog_button.setOnClickListener(this);
}
@Override
public void onClick(View view) {
switch (view.getId()) {
case R.id.button:
String inputText = editText.getText().toString();
Toast.makeText(MainActivity.this, inputText, Toast.LENGTH_SHORT).show();
imageView.setImageResource(R.drawable.img8);
break;
case R.id.progress_button:
// 修改可见性
// if (progressBar.getVisibility() == View.GONE) {
// progressBar.setVisibility(View.VISIBLE);
// } else {
// progressBar.setVisibility(View.GONE);
// }
// 测试进度
int progress = progressBar.getProgress();
progress = progress + 10;
progressBar.setProgress(progress);
// AlertDialog Demo
AlertDialog.Builder dialog = new AlertDialog.Builder(MainActivity.this);
dialog.setTitle(String.format("Title 进度---%s", progress));
dialog.setMessage(String.format("Message 进度---%s", progress));
dialog.setCancelable(false);
dialog.setPositiveButton("ok", new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialogInterface, int i) {
Log.d("MainActivity", "It's ok");
}
});
dialog.setNegativeButton("取消", new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialogInterface, int i) {
Log.d("MainActivity", "取消");
}
});
dialog.show();
break;
case R.id.progress_dialog_button:
progressDialog.setTitle("This is Progress Dialog");
progressDialog.setMessage("Loading...");
progressDialog.setCancelable(true);
progressDialog.show();
Intent intent = new Intent(MainActivity.this, ProgressDialogBack.class);
startActivityForResult(intent, 1);
break;
default:
break;
}
}
@Override
protected void onActivityResult(int requestCode, int resultCode, Intent data) {
super.onActivityResult(requestCode, resultCode, data);
switch (requestCode) {
case 1:
if (resultCode == RESULT_OK) {
String returnData = data.getStringExtra("data_return");
Log.d("MainActivity", returnData);
try {
Thread.sleep(5000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
progressDialog.dismiss();
break;
default:
}
}
}
需要注意的是,progressDialog已经被弃用,官方指南中有说明如下:
注意:Android 包含另一种名为 ProgressDialog 的对话框类,该类可显示带有进度条的对话框.不推荐使用此微件,因为它会在显示进度的情况下阻止用户与应用交互.如果需要指示加载进度或不确定的进度,您应遵循进度和 Activity 的设计指南,并在布局中使用 ProgressBar,而非 ProgressDialog.
该类已在API级别26中弃用.
ProgressDialog是模式对话框,可防止用户与应用进行交互.而不是使用此类,您应该使用进度指示器(例如)android.widget.ProgressBar,可以将其嵌入到应用的用户界面中.或者,您可以使用通知来通知用户任务的进度.
3.3 4种基本布局
3.3.1 线性布局
LinearLayout 线性布局
- android:orientation="vertical" 垂直排列
- android:orientation="horizontal" 水平排列
- android:layout_height="wrap_content" 强制性地使视图扩展以显示全部内容
- android:layout_width="match_parent" 布满整个屏幕
使用horizontal水平排列时,不能将控件的宽度指为match_parent,否则一个控件将在水平方向占满
- android:layout_gravity="center" 布局控件的对齐方式
修改activity_progress_dialog_back.xml来测试线性布局
在有属性android:layout_weight时,android:layout_width属性就不会生效,指定成0dp时规范的写法.dp是Android中用于指定控件大小\间距等单位.
android:layout_weight="1" 表示EditText和Button在水平方向平分,系统会把LinearLayout下所有的控件的layout_weight值相加,并计算每个控件占据屏幕宽度的比例.
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="horizontal">
<EditText
android:id="@+id/input_message"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:hint="Typing"
/>
<Button
android:id="@+id/send"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:text="send"/>
</LinearLayout>
为了获取更好的展示效果,修改布局文件如下:
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="horizontal">
<EditText
android:id="@+id/input_message"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:hint="Typing"
/>
<Button
android:id="@+id/send"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="send"/>
</LinearLayout>
3.3.2 相对布局
略
3.3.3 帧布局
略
3.3.4 百分比布局
略
3.4 自定义控件
View是Android中最基本的UI组件,ViewGroup是一种特殊的View,用于防止控件和布局容器.新建一个UICustomViews项目进行测试自定义组件.
3.4.1 引入布局
在layout下新建一个布局文件,起名为title.xml,代码如下:
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="@drawable/title_bg">
<Button
android:id="@+id/title_back"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:layout_margin="5dp"
android:background="@drawable/back_bg"
android:text="BACK"
android:textColor="#fff"/>
<TextView
android:id="@+id/title_text"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:layout_weight="1"
android:textColor="#fff"
android:text="Title Text"
android:textSize="24sp"/>
<Button
android:id="@+id/title_edit"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:background="@drawable/edit_bg"
android:layout_margin="5dp"
android:text="Edit"
android:textColor="#fff"/>
</LinearLayout>
在此布局中有两个按钮,一个TextView.左边的Button为返回键,右侧为编辑,中间显示标题.背景使用颜色或图片.代码中android:layout_margin="5dp"属性表示空间在上下左右方向的便宜距离.修改activity_main.xml中代码,将title.xml的布局引入,代码如下:
<?xml version="1.0" encoding="utf-8"?>
<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.java将系统中自带的标题栏隐掉,代码如下:
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();
}
}
}
3.4.2 自定义控件
引入布局解决了重复编写布局代码问题,但是如果布局中的一些控件需要进行响应事件,则需要使用自定义控件方式解决.新建TitleLayout类并继承LinearLayout,作为自定义的标题控件.代码如下:
public class TitleLayout extends LinearLayout {
public TitleLayout(Context context, @Nullable AttributeSet attrs) {
super(context, attrs);
LayoutInflater.from(context).inflate(R.layout.title, this);
}
}
重写LinearLayout中的带有两个参数的构造函数,from()方法可以构建出一个LayoutInflater对象,inflate()方法用来动态加载布局文件,inflate方法接收两个参数,一个是布局文件,一个是加载布局文件的父布局,这里为当前对象.
自定义控件创建好后,修改activity_main.xml如下:
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent">
<!-- <include layout="@layout/title"/>-->
<com.example.uicustomviews.TitleLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"/>
</LinearLayout>
添加自定义控件的时候,需要指明控件的完整类名,修改TitleLayout中的代码,给按钮注册事件,代码如下:
public class TitleLayout extends LinearLayout {
public TitleLayout(Context context, @Nullable AttributeSet attrs) {
super(context, attrs);
LayoutInflater.from(context).inflate(R.layout.title, this);
Button titleBackButton = findViewById(R.id.title_back);
Button titleEditButton = findViewById(R.id.title_edit);
titleBackButton.setOnClickListener(new OnClickListener() {
@Override
public void onClick(View view) {
((Activity) getContext()).finish();
}
});
titleEditButton.setOnClickListener(new OnClickListener() {
@Override
public void onClick(View view) {
Toast.makeText(getContext(),"You click edit button",Toast.LENGTH_SHORT).show();
}
});
}
}
此时自定义控件封装完毕.
3.5 ListView
3.5.1 ListView的简单用法
新建一个ListViewTest项目,修改activity_main.xml中代码如下:
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent">
<ListView
android:id="@+id/list_view"
android:layout_width="match_parent"
android:layout_height="match_parent"/>
</LinearLayout>
修改MainActivity.java中代码,初始化一些测试数据,
public class MainActivity extends AppCompatActivity {
private String[] data = new String[20];
private void initData() {
StringBuilder stringBuilder = new StringBuilder("Cherry");
for (int i = 0; i < 20; i++) {
data[i] = stringBuilder.append(i).toString();
stringBuilder.setLength(6);
}
}
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
initData();
ArrayAdapter<String> adapter = new ArrayAdapter<String>(
MainActivity.this, android.R.layout.simple_list_item_1, data);
ListView listView = findViewById(R.id.list_view);
listView.setAdapter(adapter);
}
}
在这里数组是无法单独传递给ListView的,需要使用Android提供的适配器实现类,这里使用的是ArrayAdapter,此外还有CursorAdapter、SimpleAdapter等.ArrayAdapter有多种构造函数,次数使用的是传入构造函数的上下文,子布局id及对应的数据.其中上下文这里为当前类,子布局id为android.R.layout.simple_list_item_1,这是Android内置的布局文件,内容只有一个TextView.最后调用setAdapter完成关联.
3.5.2 定制ListView的界面
上面的例子中,内置的子布局只有一个TextView,现在做一个自定义的ListView,为数据显示一组图片(图片文件提前复制到drawable内).首先建立一个Fruit类,用来表示数据的名称和id(此id为Android组件生成的R.id),代码如下:
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 void setName(String name) {
this.name = name;
}
public int getImageId() {
return imageId;
}
public void setImageId(int imageId) {
this.imageId = imageId;
}
}
添加一个ListView的子项的自定义布局文件,起名为fruit_item.xml.再上一个例子中使用的是Android内置的布局文件android.R.layout.simple_list_item_1.fruit_item.xml内容如下:
<?xml version="1.0" encoding="utf-8"?>
<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="40dp"
android:layout_height="40dp"/>
<TextView
android:id="@+id/fruit_name"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:layout_margin="10dp" />
</LinearLayout>
现在创建一个自定义的适配器类,并继承ArrayAdapter,通过泛型指定为Fruit类.新建的类名为FruitAdapter,代码如下:
public class FruitAdapter extends ArrayAdapter {
private int resourceId;
public FruitAdapter(@NonNull Context context, int resource, List<Fruit> objects) {
super(context, resource,objects);
resourceId = resource;
}
@Override
public View getView(int position, @Nullable View convertView, @NonNull ViewGroup parent) {
// return super.getView(position, convertView, parent);
Fruit fruit = (Fruit) getItem(position);
View view = LayoutInflater.from(getContext()).inflate(resourceId, parent, false);
ImageView fruitImage = view.findViewById(R.id.fruit_image);
TextView fruitName = view.findViewById(R.id.fruit_name);
fruitImage.setImageResource(fruit.getImageId());
fruitName.setText(fruit.getName());
return view;
}
}
FruitAdapter重写了父类的构造函数,用于将上下文、ListView的子项布局的id和数据都传递进来.另外重写了getView()方法,此方法会在每个子项目滚动到屏幕内的时候调用,在此方法中,想通过getItem()方法获取当前位置的Fruit实例,然后使用LayoutInflater来加载子布局.LayoutInflater的使用在上一个例子(3.4.1)中已经使用过,而这里的第三个参数false表示只让我们在父布局中声明的layout属性生效,但不为这个View添加父布局(这块暂时没太理解,mark).后面代码比较简单,即设置当前位置的对象的值,将传入的值和自定义 子布局中的控件关联上.
修改MainActivity中的代码如下:
public class MainActivity extends AppCompatActivity {
private String[] data = new String[20];
private List<Fruit> fruitList = new ArrayList<Fruit>(20);
private void initData() {
StringBuilder stringBuilder = new StringBuilder("Cherry");
for (int i = 0; i < 20; i++) {
data[i] = stringBuilder.append(i).toString();
stringBuilder.setLength(6);
}
}
/**
* 自定义子布局初始化数据
*/
private void initFruit(){
Fruit fruit =null;
StringBuilder stringBuilder = new StringBuilder("Cherry");
StringBuilder drawableName = new StringBuilder("shuzi");
for (int i = 1; i < 21; i++) {
int resID = getResources().getIdentifier(drawableName.append(i).toString() , "drawable", getPackageName());
fruit = new Fruit(stringBuilder.append(i).toString(),resID);
fruitList.add(fruit);
stringBuilder.setLength(6);
drawableName.setLength(5);
}
}
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
// initData();
//
// ArrayAdapter<String> adapter = new ArrayAdapter<String>(
// MainActivity.this, android.R.layout.simple_list_item_1, data);
// ListView listView = findViewById(R.id.list_view);
// listView.setAdapter(adapter);
initFruit();
FruitAdapter fruitAdapter = new FruitAdapter(MainActivity.this,R.layout.fruit_item,fruitList);
ListView listview = findViewById(R.id.list_view);
listview.setAdapter(fruitAdapter);
}
}
这里为了方便动态的加载图片文件名称,使用了以下方式来获取图片的id
int resID = getResources().getIdentifier(drawableName.append(i).toString() , "drawable", getPackageName());
如果单独初始化一个对象,应该按以下方式使用:
Fruit fruit = new Fruit("水果",R.drawable.xxx);
3.5.3 提升ListView的运行效率
在3.5.2的例子中,FruitAdapter的getView()方法每次都将布局重新加载了一遍,当ListView快速滚动式,可能造成系统瓶颈.在getView方法中有一个convertView的参数,这个参数可以将布局进行缓存,修改FruitAdapter
public class FruitAdapter extends ArrayAdapter {
...
@Override
public View getView(int position, @Nullable View convertView, @NonNull ViewGroup parent) {
Fruit fruit = (Fruit) getItem(position);
View view;
// View view = LayoutInflater.from(getContext()).inflate(resourceId, parent, false);
if (convertView == null) {
view = LayoutInflater.from(getContext()).inflate(resourceId, parent, false);
}else {
view = convertView;
}
ImageView fruitImage = view.findViewById(R.id.fruit_image);
TextView fruitName = view.findViewById(R.id.fruit_name);
fruitImage.setImageResource(fruit.getImageId());
fruitName.setText(fruit.getName());
return view;
}
}
此时,可以提高ListView的运行效率.但在获取fruitImage和fruitName时仍然回去访问控件实例,此时可以借助新建一个ViewHolder的类进行性能优化,在FruitAdapter中新建内部类ViewHolder,完整代码如下:
public class FruitAdapter extends ArrayAdapter {
private int resourceId;
public FruitAdapter(@NonNull Context context, int resource, List<Fruit> objects) {
super(context, resource, objects);
resourceId = resource;
}
@Override
public View getView(int position, @Nullable View convertView, @NonNull ViewGroup parent) {
Fruit fruit = (Fruit) getItem(position);
View view;
ViewHolder viewHolder;
// View view = LayoutInflater.from(getContext()).inflate(resourceId, parent, false);
if (convertView == null) {
view = LayoutInflater.from(getContext()).inflate(resourceId, parent, false);
viewHolder = new ViewHolder();
viewHolder.imageView = view.findViewById(R.id.fruit_image);
viewHolder.textView = view.findViewById(R.id.fruit_name);
view.setTag(viewHolder);//将ViewHolder存储到View中
}else {
view = convertView;
viewHolder = (ViewHolder) view.getTag();
}
viewHolder.imageView.setImageResource(fruit.getImageId());
viewHolder.textView.setText(fruit.getName());
// ImageView fruitImage = view.findViewById(R.id.fruit_image);
// TextView fruitName = view.findViewById(R.id.fruit_name);
// fruitImage.setImageResource(fruit.getImageId());
// fruitName.setText(fruit.getName());
return view;
}
class ViewHolder{
ImageView imageView;
TextView textView;
}
}
setTag()方法的介绍如下,按照个人理解,使用View的Tag可以存储一些数据对象而不需要自定义其他的数据结构,并在需要时通过getTag取出来,这样就完成了数据的缓存
/**
* Sets the tag associated with this view. A tag can be used to mark
* a view in its hierarchy and does not have to be unique within the
* hierarchy. Tags can also be used to store data within a view without
* resorting to another data structure.
*
* @param tag an Object to tag the view with
*
* @see #getTag()
* @see #setTag(int, Object)
*/
public void setTag(final Object tag) {
mTag = tag;
}
3.5.4 ListView的点击事件
如果ListView中的子项不能点击的话就没什么意义了,修改MainActivity中代码响应点击事件
public class MainActivity extends AppCompatActivity {
...
private List<Fruit> fruitList = new ArrayList<Fruit>(20);
...
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
// initData();
//
// ArrayAdapter<String> adapter = new ArrayAdapter<String>(
// MainActivity.this, android.R.layout.simple_list_item_1, data);
// ListView listView = findViewById(R.id.list_view);
// listView.setAdapter(adapter);
initFruit();
FruitAdapter fruitAdapter = new FruitAdapter(MainActivity.this,R.layout.fruit_item,fruitList);
ListView listview = findViewById(R.id.list_view);
listview.setAdapter(fruitAdapter);
// 子布局的点击事件
listview.setOnItemClickListener(new AdapterView.OnItemClickListener() {
@Override
public void onItemClick(AdapterView<?> parent, View view, int position, long id) {
String imgMessage = String.format("Click img %s", position+1);
Fruit fruit = fruitList.get(position);
Toast.makeText(MainActivity.this,fruit.getName(),Toast.LENGTH_SHORT).show();
Log.d("MainActivity",fruit.getName());
}
});
}
}
此时当点击ListView中的任何一个子项目时,就会调用onItemClick方法,根据position来判断用户点击的是哪个子项目.这里的position和id分别表示view的位置和row的id,如下介绍
/**
* Callback method to be invoked when an item in this AdapterView has
* been clicked.
* <p>
* Implementers can call getItemAtPosition(position) if they need
* to access the data associated with the selected item.
*
* @param parent The AdapterView where the click happened.
* @param view The view within the AdapterView that was clicked (this
* will be a view provided by the adapter)
* @param position The position of the view in the adapter.
* @param id The row id of the item that was clicked.
*/
void onItemClick(AdapterView<?> parent, View view, int position, long id);
根据以上例子,考虑如果想在子布局的每一项单独在绑定一个事件,那么在FruitAdapter中实现、在MainActivity中实现、两个类中都实现,那么响应用户点击的顺序是什么样呢?于是修改代码FruitAdapter及MainActivity如下所示:
FruitAdapter.java
public class FruitAdapter extends ArrayAdapter {
private int resourceId;
public FruitAdapter(@NonNull Context context, int resource, List<Fruit> objects) {
super(context, resource, objects);
resourceId = resource;
}
@Override
public View getView(int position, @Nullable View convertView, @NonNull ViewGroup parent) {
Fruit fruit = (Fruit) getItem(position);
View view;
ViewHolder viewHolder;
// View view = LayoutInflater.from(getContext()).inflate(resourceId, parent, false);
if (convertView == null) {
view = LayoutInflater.from(getContext()).inflate(resourceId, parent, false);
viewHolder = new ViewHolder();
viewHolder.imageView = view.findViewById(R.id.fruit_image);
viewHolder.textView = view.findViewById(R.id.fruit_name);
view.setTag(viewHolder);//将ViewHolder存储到View中
}else {
view = convertView;
viewHolder = (ViewHolder) view.getTag();
}
viewHolder.imageView.setImageResource(fruit.getImageId());
viewHolder.textView.setText(fruit.getName());
//----
//测试绑定点击响应
viewHolder.imageView.setOnClickListener(v -> Log.d("FruitAdapter","FruitAdapter click event"));
//-----
// ImageView fruitImage = view.findViewById(R.id.fruit_image);
// TextView fruitName = view.findViewById(R.id.fruit_name);
// fruitImage.setImageResource(fruit.getImageId());
// fruitName.setText(fruit.getName());
return view;
}
class ViewHolder{
ImageView imageView;
TextView textView;
}
}
MainActivity.java
public class MainActivity extends AppCompatActivity {
private String[] data = new String[20];
private List<Fruit> fruitList = new ArrayList<Fruit>(20);
private void initData() {
StringBuilder stringBuilder = new StringBuilder("Cherry");
for (int i = 0; i < 20; i++) {
data[i] = stringBuilder.append(i).toString();
stringBuilder.setLength(6);
}
}
/**
* 自定义子布局初始化数据
*/
private void initFruit(){
Fruit fruit =null;
StringBuilder stringBuilder = new StringBuilder("Cherry");
StringBuilder drawableName = new StringBuilder("shuzi");
for (int i = 1; i < 21; i++) {
int resID = getResources().getIdentifier(drawableName.append(i).toString() , "drawable", getPackageName());
fruit = new Fruit(stringBuilder.append(i).toString(),resID);
fruitList.add(fruit);
stringBuilder.setLength(6);
drawableName.setLength(5);
}
}
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
// initData();
//
// ArrayAdapter<String> adapter = new ArrayAdapter<String>(
// MainActivity.this, android.R.layout.simple_list_item_1, data);
// ListView listView = findViewById(R.id.list_view);
// listView.setAdapter(adapter);
initFruit();
FruitAdapter fruitAdapter = new FruitAdapter(MainActivity.this,R.layout.fruit_item,fruitList);
ListView listview = findViewById(R.id.list_view);
listview.setAdapter(fruitAdapter);
// 子布局的点击事件
listview.setOnItemClickListener(new AdapterView.OnItemClickListener() {
@Override
public void onItemClick(AdapterView<?> parent, View view, int position, long id) {
String imgMessage = String.format("Click img %s", position+1);
Fruit fruit = fruitList.get(position);
Toast.makeText(MainActivity.this,fruit.getName(),Toast.LENGTH_SHORT).show();
Log.d("MainActivity",fruit.getName());
//---
//测试获取子组件绑定点击事件
// ImageView imageView = view.findViewById(R.id.fruit_image);
ImageView imageView = (ImageView) ((LinearLayout)view).getChildAt(0);
imageView.setOnClickListener(v -> {
Log.d("MainActivity",imgMessage);
Toast.makeText(MainActivity.this,imgMessage,Toast.LENGTH_SHORT).show();
});
//-----
}
});
}
}
测试发现如果在FruitAdapter中和MainActivity中同时加入imageView的点击,并不会多次执行,但是每次点击时,响应的事件却不同.此处作为遗留问题,后续继续研究下.
3.6 RecyclerView
ListView需要优化运行效率,同时只能实现纵向滚动,无法横向滚动.而RecyclerView可以说是一个增强的ListView.新建一个RecycleViewTest项目来测试.
3.6.1 RecyclerView的基本用法
原书中描述RecyclerView定义在了support库中,但是本次测试时基于android9(API-28),所以略有差异.根据官方指南,在gradle中增加依赖,如下.增加后需要同步一下.
dependencies {
implementation "androidx.recyclerview:recyclerview:1.1.0"
// For control over item selection of both touch and mouse driven selection
implementation "androidx.recyclerview:recyclerview-selection:1.1.0-rc01"
}
修改activity_main.xml中代码,修改为LinearLayout布局并增加RecyclerView组件,代码如下:
<?xml version="1.0" encoding="utf-8"?>
<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:id="@+id/recycler_view"
android:layout_width="match_parent"
android:layout_height="match_parent"/>
</LinearLayout>
在上面代码中,因为RecyclerView不是SDK内置组件,所以需要指定完整的包路径.
之后将3.5中的Fruit与fruit_item.xml的布局文件及图片文件复制进来即可.新建一个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 fruitText;
public ViewHolder(@NonNull View itemView) {
super(itemView);
fruitImage = itemView.findViewById(R.id.fruit_image);
fruitText = itemView.findViewById(R.id.fruit_name);
}
}
public FruitAdapter(List<Fruit> fruitList) {
this.mFruitList = fruitList;
}
@NonNull
@Override
public ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
View view = LayoutInflater.from(parent.getContext()).inflate(R.layout.fruit_item, parent, false);
ViewHolder viewHolder = new ViewHolder(view);
return viewHolder;
}
@Override
public void onBindViewHolder(@NonNull ViewHolder holder, int position) {
Fruit fruit = mFruitList.get(position);
holder.fruitImage.setImageResource(fruit.getImageId());
holder.fruitText.setText(fruit.getName());
}
@Override
public int getItemCount() {
return mFruitList.size();
}
}
首先,内部类ViewHolder,这个ViewHolder需要继承RecyclerView.ViewHolder.同时构造方法需要传入一个View参数,这个参数对应的通常是RecyclerView子项的最外层布局,这样就可以通过findViewById()方法来获取布局中的ImageView和TextView组件实例.
FruitAdapter也有一个构造函数,用来传入数据源,这里是mFruitList.
FruitAdapter继承了RecyclerView.Adapter,那么就必须重写onCreateViewHolder()、onBindViewHolder()、getItemCount()这三个方法.
- onCreateViewHolder() 用来创建ViewHolder实例,并把加载的布局传入到构造函数中,最后返回对应的泛型实例,这里是ViewHolder
- onBindViewHolder() 用来对子项数据进行赋值,会在每个子项数据滚动到屏幕内时执行,通过position参数获取当前的实例对象
- getItemCount() 用来告诉RecyclerView数据一共有多少
修改MainActivity中代码,并测试结果
public class MainActivity extends AppCompatActivity {
private List<Fruit> fruitList = new ArrayList<>(20);
/**
* 自定义子布局初始化数据
*/
private void initFruits(){
Fruit fruit =null;
String nameStr = "Cherry%s";
StringBuilder drawableName = new StringBuilder("shuzi");
for (int i = 1; i < 21; i++) {
int resID = getResources().getIdentifier(drawableName.append(i).toString() , "drawable", getPackageName());
fruit = new Fruit(String.format(nameStr,i),resID);
fruitList.add(fruit);
drawableName.setLength(5);
}
}
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
initFruits();
RecyclerView recyclerView = findViewById(R.id.recycler_view);
LinearLayoutManager linearLayoutManager = new LinearLayoutManager(this);
recyclerView.setLayoutManager(linearLayoutManager);
FruitAdapter fruitAdapter = new FruitAdapter(fruitList);
recyclerView.setAdapter(fruitAdapter);
}
}
LayoutManager用于指定RecyclerView的布局方式,这里使用的是LIneraLayoutManager.S
3.6.2 实现横向滚动和瀑布流布局
首先对fruit_item.xml布局进行修改,因为目前的这个里的布局是水平排列,适用于纵向滚动的场景.要实现横向滚动,则需要把fruit_item中的布局改为垂直排列比较合理,修改fruit_item.xml如下:
<?xml version="1.0" encoding="utf-8"?>
<!--<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"-->
<!-- android:layout_width="match_parent"-->
<!-- android:layout_height="wrap_content">-->
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="100dp"
android:layout_height="wrap_content"
android:orientation="vertical">
<ImageView
android:id="@+id/fruit_image"
android:layout_width="40dp"
android:layout_height="40dp"
android:background="#85C1E9"
android:layout_gravity="center"/>
<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,之后将ImageView与TextView的布局改为水平居中.修改MainActivity中代码,将布局管理器中的布局改为Horizon.这里是将LinearLayoutManager中设置为Horizonal.代码如下:
public class MainActivity extends AppCompatActivity {
...
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
initFruits();
RecyclerView recyclerView = findViewById(R.id.recycler_view);
LinearLayoutManager linearLayoutManager = new LinearLayoutManager(this);
linearLayoutManager.setOrientation(LinearLayoutManager.HORIZONTAL);
recyclerView.setLayoutManager(linearLayoutManager);
FruitAdapter fruitAdapter = new FruitAdapter(fruitList);
recyclerView.setAdapter(fruitAdapter);
}
...
}
在RecyclerView中,将布局的排列交给了LayoutManager管理,子类只需要按照LayoutManager的规范了来定义就可以实现不通的排列布局.
除了LinearLayout外,RecyclerView还提供了GridLayoutManager和StaggeredGridManager这两种内置的布局排列.GirdLayoutManager用于实现网格的布局,StaggeredGridLayoutManager用来实现瀑布流式布局.下面来实现一个瀑布流布局.
复制一个fruit_item.xml起名为fruit_item_staggered.xml,修改其中代码如下:
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:orientation="vertical"
android:layout_height="wrap_content"
android:layout_margin="5dp">
<ImageView
android:id="@+id/fruit_image"
android:layout_width="40dp"
android:layout_height="40dp"
android:background="#85C1E9"
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>
这里将宽度修改为了match_parent,因为瀑布流布局的宽度应该有布局的列数来自动计算.
修改FruitAdapter.java中布局的引用
@NonNull
@Override
public ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
View view = LayoutInflater.from(parent.getContext()).inflate(R.layout.fruit_item_staggered, parent, false);
// View view = LayoutInflater.from(parent.getContext()).inflate(R.layout.fruit_item, parent, false);
ViewHolder viewHolder = new ViewHolder(view);
return viewHolder;
}
修改MainActivity.java中的布局管理器部分,
public class MainActivity extends AppCompatActivity {
private List<Fruit> fruitList = new ArrayList<>(20);
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();
}
/**
* 自定义子布局初始化数据
*/
private void initFruits(){
Fruit fruit =null;
String nameStr = "Cherry%s";
StringBuilder drawableName = new StringBuilder("shuzi");
for (int i = 1; i < 21; i++) {
int resID = getResources().getIdentifier(drawableName.append(i).toString() , "drawable", getPackageName());
fruit = new Fruit(getRandomLengthName(String.format(nameStr,i)),resID);
fruitList.add(fruit);
drawableName.setLength(5);
}
}
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
initFruits();
RecyclerView recyclerView = findViewById(R.id.recycler_view);
// LinearLayoutManager linearLayoutManager = new LinearLayoutManager(this);
// linearLayoutManager.setOrientation(LinearLayoutManager.HORIZONTAL);
// recyclerView.setLayoutManager(linearLayoutManager);
StaggeredGridLayoutManager staggeredGridLayoutManager = new StaggeredGridLayoutManager(3, StaggeredGridLayoutManager.VERTICAL);
recyclerView.setLayoutManager(staggeredGridLayoutManager);
FruitAdapter fruitAdapter = new FruitAdapter(fruitList);
recyclerView.setAdapter(fruitAdapter);
}
}
这里创建了一个StaggeredGridLayoutManager 的布局管理器,第一个参数3表示布局将分3列,第二个参数指定布局的排列方向.getRandomLengthName()方法用来创建一个随机数并修改Fruit的名字,将名字变得长短不一,来测试子项的高度.
在测试一下GridLayoutManager,同样复制一下fruit_item.xml起名字为fruit_item_grid.xml,内容如下:
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:orientation="vertical"
android:layout_height="wrap_content"
android:layout_margin="5dp">
<ImageView
android:id="@+id/fruit_image"
android:layout_width="40dp"
android:layout_height="40dp"
android:background="#85C1E9"
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>
修改FruitAdapter.java中布局的引用
@NonNull
@Override
public ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
View view = LayoutInflater.from(parent.getContext()).inflate(R.layout.fruit_item_grid, parent, false);
// View view = LayoutInflater.from(parent.getContext()).inflate(R.layout.fruit_item_staggered, parent, false);
// View view = LayoutInflater.from(parent.getContext()).inflate(R.layout.fruit_item, parent, false);
ViewHolder viewHolder = new ViewHolder(view);
return viewHolder;
}
修改MainActivity.java中的布局管理器部分,
public class MainActivity extends AppCompatActivity {
...
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
initFruits();
RecyclerView recyclerView = findViewById(R.id.recycler_view);
// LinearLayoutManager linearLayoutManager = new LinearLayoutManager(this);
// linearLayoutManager.setOrientation(LinearLayoutManager.HORIZONTAL);
// recyclerView.setLayoutManager(linearLayoutManager);
// StaggeredGridLayoutManager staggeredGridLayoutManager = new StaggeredGridLayoutManager(3, StaggeredGridLayoutManager.VERTICAL);
// recyclerView.setLayoutManager(staggeredGridLayoutManager);
GridLayoutManager gridLayoutManager = new GridLayoutManager(MainActivity.this,4);
recyclerView.setLayoutManager(gridLayoutManager);
FruitAdapter fruitAdapter = new FruitAdapter(fruitList);
recyclerView.setAdapter(fruitAdapter);
}
}
这里的GridLayoutManager构造方法,根据API的描述,第一个参数为当前的Context,第二个参数为Grid的列数
/**
* Creates a vertical GridLayoutManager
*
* @param context Current context, will be used to access resources.
* @param spanCount The number of columns in the grid
*/
public GridLayoutManager(Context context, int spanCount) {
super(context);
setSpanCount(spanCount);
}
3.6.3 RecyclerView的点击事件
在FruitAdapter.java中修改RecyclerView的注册点击事件,修改代码如下:
public class FruitAdapter extends RecyclerView.Adapter<FruitAdapter.ViewHolder> {
private List<Fruit> mFruitList;
static class ViewHolder extends RecyclerView.ViewHolder {
View fruitView;
ImageView fruitImage;
TextView fruitText;
public ViewHolder(@NonNull View itemView) {
super(itemView);
this.fruitView = itemView;
fruitImage = itemView.findViewById(R.id.fruit_image);
fruitText = itemView.findViewById(R.id.fruit_name);
}
}
public FruitAdapter(List<Fruit> fruitList) {
this.mFruitList = fruitList;
}
@NonNull
@Override
public ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, final int viewType) {
View view = LayoutInflater.from(parent.getContext()).inflate(R.layout.fruit_item_grid, parent, false);
// View view = LayoutInflater.from(parent.getContext()).inflate(R.layout.fruit_item_staggered, parent, false);
// View view = LayoutInflater.from(parent.getContext()).inflate(R.layout.fruit_item, parent, false);
// ViewHolder viewHolder = new ViewHolder(view);
final ViewHolder viewHolder = new ViewHolder(view);
viewHolder.fruitView.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View view) {
int position = viewHolder.getAdapterPosition();
Fruit fruit = mFruitList.get(position);
Toast.makeText(view.getContext(), "you click view" + fruit.getName(), Toast.LENGTH_SHORT).show();
}
});
viewHolder.fruitImage.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View view) {
int position = viewHolder.getAdapterPosition();
Fruit fruit = mFruitList.get(position);
Toast.makeText(view.getContext(),"You click Image"+fruit.getName(),Toast.LENGTH_SHORT).show();
}
});
return viewHolder;
}
@Override
public void onBindViewHolder(@NonNull ViewHolder holder, int position) {
Fruit fruit = mFruitList.get(position);
holder.fruitImage.setImageResource(fruit.getImageId());
holder.fruitText.setText(fruit.getName());
}
@Override
public int getItemCount() {
return mFruitList.size();
}
}
在内部类ViewHolder中增加了一个fruitVIew的变量来保存子组件的最外层实例,这样在监听点击动作时可以监听到最外层的组件.这里分别注册的最外层组件的点击事件及图片部分的点击事件.当用户点击不通区域时,则调用不同的事件.用户点击文字部分时,因为没有注册任何事件,则此事件被最外层布局捕捉到.
3.7 编写界面的最佳实践
这里需要先创建一个UIBestPractice的项目,修改activity_main.xml如下:
<?xml version="1.0" encoding="utf-8"?>
<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>
其中message_left如果使用普通的图片,则由于宽度问题图片可能被拉伸,此时的效果非常差,所以需要使用Android SDK自带的draw9patch来编辑图片
3.7.1 制作Nine-Patch图片
书中描述在Android Studio内置的jdk中有对应的工具,为Android Studio安装目录>/jre/bin,但测试时没有找到.将图片复制到idea中,选中图片,右键.此时有create 9-path file的选项,通过此功能也可以制作Nine-Patch图片.
自己测试的几个图片,不太熟练.直接下载书中源码里图片继续测试.
3.7.2 聊天界面
聊天界面的例子主要分两个部分,一个是RecyclerVeiw实现的子组件来显示消息,一个是通过Text和Button组成的发送消息.修改activity_main.xml的布局文件如下:
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:orientation="vertical"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="#d8e0e8">
<androidx.recyclerview.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/input_text"
android:layout_width="0dp"
android:layout_weight="1"
android:hint="Type something here"
android:layout_height="wrap_content"
android:maxLines="2"/>
<Button
android:id="@+id/send"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Send"/>
</LinearLayout>
</LinearLayout>
创建一个实体类Msg来定义消息,主要属性有消息内容及消息类型(接收的消息和发送的消息),Msg.java如下
public class Msg {
public static final int TYPE_RECEIVED = 0;
public static final int TYPE_SEND = 1;
private String content;
private int type;
public Msg(String content, int type) {
this.content = content;
this.type = type;
}
... setter and getter
}
根据RecyclerView的使用方式,需要一个Adapter和layoutManager.先创建一个MsgAdapter类,在类中实现ViewHolder的静态类及存放消息列表的属性,代码如下:
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(@NonNull View view) {
super(view);
leftlayout = view.findViewById(R.id.left_layout);
rightLayout = view.findViewById(R.id.right_layout);
leftMsg = view.findViewById(R.id.left_msg);
rightMsg = view.findViewById(R.id.right_msg);
}
}
public MsgAdapter(List<Msg> mMsgList) {
this.mMsgList = mMsgList;
}
@NonNull
@Override
public ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
View view = LayoutInflater.from(parent.getContext()).inflate(R.layout.msg_item, parent, false);
return new ViewHolder(view);
}
@Override
public void onBindViewHolder(@NonNull ViewHolder holder, int position) {
Msg msg = mMsgList.get(position);
if (msg.getType() == Msg.TYPE_RECEIVED) {
holder.leftlayout.setVisibility(View.VISIBLE);
holder.rightLayout.setVisibility(View.GONE);
holder.leftMsg.setText(msg.getContent());
} else {
holder.leftlayout.setVisibility(View.GONE);
holder.rightLayout.setVisibility(View.VISIBLE);
holder.rightMsg.setText(msg.getContent());
}
}
@Override
public int getItemCount() {
return mMsgList.size();
}
}
这里和上节中的例子基本一致.只是在onBindViewHolder()中增加了对消息类型的判断,如果是收到消息,则显示左侧消息布局,隐藏右侧.如果是发出的消息,就隐藏左侧显示右侧.现在修改MainActivity.java中的代码,
public class MainActivity extends AppCompatActivity {
private RecyclerView recyclerView;
private EditText inputText;
private Button sendButton;
private MsgAdapter msgAdapter;
private List<Msg> msgList = new ArrayList<>();
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
recyclerView = findViewById(R.id.msg_recycler_view);
inputText = findViewById(R.id.input_text);
sendButton = findViewById(R.id.send);
LinearLayoutManager layoutManager = new LinearLayoutManager(this);
recyclerView.setLayoutManager(layoutManager);
msgAdapter = new MsgAdapter(msgList);
recyclerView.setAdapter(msgAdapter);
sendButton.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View view) {
String content = inputText.getText().toString();
if (!"".equals(content)) {
Msg msg = new Msg(content, getRandomType());
msgList.add(msg);
// 当有新消息时刷新ListView中的显示
msgAdapter.notifyItemInserted(msgList.size() - 1);
// 将ListView定位到最后一行
recyclerView.scrollToPosition(msgList.size() - 1);
inputText.setText("");
}
}
});
}
//随机获取消息类型
private int getRandomType() {
Random random = new Random();
if (random.nextInt(10) > 5) {
return Msg.TYPE_SEND;
} else {
return Msg.TYPE_RECEIVED;
}
}
}
这里增加了一个getRandomType的方法,用来随机生成消息是发送的还是接收的.
遗留的问题
- 笔记3.5.4 ListView的点击事件中,子布局中子组件点击事件加载顺序及是否会覆盖的问题