参考书籍:Android第一行代码(第二版).郭霖著
1、常用控件
系统会对Button中的所有英文字母自动进行大写转换,可以通过设置android: textAllCaps="false"属性来禁用。
Android控件的使用基本都很相似:定义id、指定控件宽高、加入控件特有属性。
EditText 特有属性: hint、maxlines
ImageView: 展示图片,特有属性:src,可在程序中通过代码动态更改图片(使用setImageResource()方法);由于drawable目录没指定具体分辨率,一般不用它放置图片。可在res下新建一个drawable-xhdpi目录来存放。
ProgressBar: 控件的可见属性设置:所有的Android控件都有这个属性,通过android:visibility指定。值有三种:visible、invisible(不可见)、gone(消失),还可在代码中通过setVisibility()方法来设置(值:View.VISIBLE/View.INVISIBLE/View.GONE)。特有属性:style,可指定进度条样式(默认为圆形,可设为水平,同时通过max设置最大值,然后在代码中动态更改进度getProgress()/setProgress())
AlertDialog: 置顶于所有界面元素之上,能屏蔽其他控件交互能力,用于提示/警告。在程序中通过编码使用:首先通过AlertDialog.Builder创建一个AlertDialog实例,然后设置标题、内容、可否取消等属性,再调用setPositiveButton()和setNegativeButton()方法设置确定和取消按钮的点击事件,最后调用show()方法显示。
ProgressDialog:与AlertDialog类似,但显示进度条,一般用于表示当前操作比较耗时,需耐心等待。用法:用ProgressDialog构建对象,同样设置标题、内容、可否取消等属性,最后调用show()显示。当setCancelable()中传入false时表示不能通过Back键取消,这时须在代码中做好控制,数据加载完后必须调用ProgressDialog的dismiss()方法关闭对话框,否则对话框会一直存在。
2、基本布局
4种基本布局:
线性布局Linear Layout:有水平和垂直两种方向。android: layout_gravity属性指定控件在布局中的对齐方式,android: gravity属性指定文字在空间中的对齐方式,两者的可选值差不多。但当Linear Layout方向为水平时,只有垂直方向上的对齐方式才生效,垂直方向类推。android: layout_weight属性可指定控件比例(layout_width设置为0dp),下面两种布局不支持此功能。
相对布局RelativeLayout: 相对父控件进行定位、相对控件进行定位(两组:位于控件的上下左右方、与控件的上下左右边缘对齐)
帧布局FrameLayout: 所有控件默认摆放在左上角。可通过layout_gravity指定控件的对齐方式(与Linear Layout相似)。应用场景较少。
百分比布局(新增布局,定义在support库中)PercentFrameLayout和PercentRelativeLayout: 要使新增布局在所有版本上都能使用,只需在项目的build.gradle中添加百分比布局库的依赖(能保证百分比布局在Android所有系统版本上的兼容性)
dependencies {
compile fileTree(dir: 'libs', include: ['*.jar'])
androidTestCompile('com.android.support.test.espresso:espresso-core:2.2.2', {
exclude group: 'com.android.support', module: 'support-annotations'
})
compile 'com.android.support:appcompat-v7:24.2.1'
compile 'com.android.support:percent:24.2.1'
testCompile 'junit:junit:4.12'
}
同步即可。
控件的宽高使用了app命名空间自定义的属性。PercentFrameLayout还是会集成FrameLayout的属性,所有控件默认摆放在布局的左上角,为了不重叠还是使用了layout_gravity错位。效果如下:
同样,PercentRelativeLayout继承了RelativeLayout中所有属性,并可以使用app:layout_widthPercent和app: layout_heightPercent分别指定控件宽高。
还有AbsoluteLayout、TableLayout等布局,但使用得很少。
3、自定义控件
控件和布局的继承结构:
由图可知,所有控件直接或间接继承自View,所有布局直接或间接继承自ViewGroup。View时Android中最基本的UI组件,可在屏幕上绘制一块矩形区,并能响应这块矩形区的各种事件,因此各种控件其实就是在View的基础之上又添加了各自特有的功能。ViewGroup则是一种特殊的View,它可以包含很多子View和子ViewGroup,是一个用于放置控件和布局的容器。
两种创建自定义控件的方法:
(1)引入布局
当多个活动界面需要同一个自定义布局控件时,可通过引入布局方式来解决重复问题。如:自定义一个标题栏如下:
在程序中使用此标题栏,修改活动布局文件,如下:
通过include引入标题栏布局即可。最后记得在活动中将系统自带的标题栏隐藏掉(否则两个标题栏都会显示):
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.main_layout);
ActionBar actionbar = getSupportActionBar();
if(actionbar != null){
actionbar.hide();
}
}
效果如下:
(2)创建自定义控件
如果一些控件需要响应事件,且不管在哪个活动中功能都相同,可用自定义控件的方式解决。
新建如下控件类:
public class TitleLayout extends LinearLayout {
public TitleLayout(Context context, AttributeSet attrs){//在布局中引入此控件就会调用此构造函数
super(context, attrs);
LayoutInflater.from(context).inflate(R.layout.title,this);
//inflate有两个参数:动态加载一个布局文件,给加载的布局文件添加一个父布局
}
}
接下来需在布局文件中添加此自定义控件:
需用完整类名。为标题栏中的按钮注册点击事件,修改TitleLayout中代码:
public class TitleLayout extends LinearLayout {
public TitleLayout(Context context, AttributeSet attrs){//在布局中引入此控件就会调用此构造函数
super(context, attrs);
LayoutInflater.from(context).inflate(R.layout.title,this);
//inflate有两个参数:动态加载一个布局文件,给加载的布局文件添加一个父布局
Button titleBack = (Button) findViewById(R.id.title_back);
Button titleEdit = (Button) findViewById(R.id.title_edit);
titleBack.setOnClickListener(new OnClickListener(){
@Override
public void onClick(View v) {
((Activity) getContext()).finish();
}
});
titleEdit.setOnClickListener(new OnClickListener() {
@Override
public void onClick(View v) {
Toast.makeText(getContext(),"You clicked Edit button",Toast.LENGTH_SHORT).show();
}
});
}
}
这样,每当引入TitleLayout时,返回按钮和编辑按钮的点击事件就已经自动实现好了。
最常用控件之一。
(1)简单用法:
布局中插入此控件,再在程序中控制其显示内容和形式。
在activity中,通过适配器将数据与其关联:
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 adapter = new ArrayAdapter(MainActivity.this,
android.R.layout.simple_list_item_1,data);//ArrayAdapter适配器有多个构造函数重载,根据实际选择
//这里的参数分别为当前上下文、ListView子项布局的id、要适配的数据
ListView listView = (ListView) findViewById(R.id.list_view);//数据无法直接传递给listView,需借助适配器完成
listView.setAdapter(adapter);
}
效果如下:
(2)定制ListView的界面
上述方法只能显示一段文本,太单调,可通过定制界面显示更加丰富的内容。
首先为要显示的数据定义一个实体类,作为ListView适配器的适配类型。新建类Fruit,如下:
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子项指定一个自定义布局:
接下来需创建自定义适配器,继承自ArrayAdapter,泛型为Fruit类:
public class FruitAdapter extends ArrayAdapter {
private int resourceId;
public FruitAdapter(Context context, int textViewResourceId, List objects){
super(context, textViewResourceId, objects);
resourceId = textViewResourceId;
}
@NonNull
@Override
public View getView(int position, View convertView, ViewGroup parent) {//每个子项被滚动到屏幕内时被调用
Fruit fruit = getItem(position);//获取当前的Fruit实例
View view = LayoutInflater.from(getContext()).inflate(resourceId, parent,false);//标准写法
//false表示只让在父布局中声明的layout属性生效,但不为这个view添加父布局(有了父布局就不能再添加到ListView中)
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;
}
}
然后修改MainActivity中的代码:
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 listView = (ListView) findViewById(R.id.list_view);//数据无法直接传递给listView,需借助适配器完成
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 mango = new Fruit("Mango",R.drawable.mango_pic);
fruitList.add(mango);
}
}
(3)提升运行效率
ListView有很多细节可以优化,其中运行效率就是很重要的一点。目前再FruitAdapter的getView()方法中每次都将布局重新加载一遍,当快速滚动时会成为性能瓶颈。可利用convert View参数缓存布局,对getView()做如下修改:
public View getView(int position, View convertView, ViewGroup parent) {//每个子项被滚动到屏幕内时被调用
Fruit fruit = getItem(position);//获取当前的Fruit实例
View view ;
if (convertView == null){
view = LayoutInflater.from(getContext()).inflate(resourceId, parent,false);
}else {
view = convertView;
}
获取控件实例代码也会重复调用,还可继续优化,可借助一个ViewHolder进行优化,修改FruitAdapter代码:
public View getView(int position, View convertView, ViewGroup parent) {//每个子项被滚动到屏幕内时被调用
Fruit fruit = getItem(position);//获取当前的Fruit实例
View view ;
ViewHolder viewHolder;
if (convertView == null){
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
}
viewHolder.fruitImage.setImageResource(fruit.getImageId());
viewHolder.fruitName.setText(fruit.getName());
return view;
}
class ViewHolder{
ImageView fruitImage;
TextView fruitName;
}
}
响应点击事件,在MainActivity中添加点击响应程序:
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 listView = (ListView) findViewById(R.id.list_view);//数据无法直接传递给listView,需借助适配器完成
listView.setAdapter(adapter);
listView.setOnItemClickListener(new AdapterView.OnItemClickListener(){
@Override
public void onItemClick(AdapterView> parent, View view, int position, long id) {//
用户点击ListView中任一子项时会回调,通过position判断点击的哪个子项
Fruit fruit = fruitList.get(position);
Toast.makeText(MainActivity.this,fruit.getName(),Toast.LENGTH_SHORT).show();
}
});
}
效果如下:
ListView虽然功能强大,但需要一些技巧提升运行效率,扩展性也不够好(只能实现纵向滚动效果,不能横向)。所以Android提供了更强大的滚动控件——RecyclerView,可轻松实现ListView同样效果,还优化了各种不足。
(1)基本用法:
和百分比布局类似,属于新增控件,同样定义在support库中,所以要使用此控件需要在项目的build.gradle中添加相应依赖库。
打开app/build.gradle文件,在dependencies闭包中添加如下内容:
dependencies {
compile fileTree(dir: 'libs', include: ['*.jar'])
androidTestCompile('com.android.support.test.espresso:espresso-core:2.2.2', {
exclude group: 'com.android.support', module: 'support-annotations'
})
compile 'com.android.support:appcompat-v7:24.2.1'
compile 'com.adnroid.support:recyclerview-v7:24.2.1'
testCompile 'junit:junit:4.12'
}
点击同步。修改布局文件:
不是内置在系统SDK中的,需写完整包名。同样将图片、Fruit类、子项布局文件fruit_item.xml准备好。
接下来需为其准备一个适配器,新建FruitAdapter继承自RecyclerView.Adapter,并将泛型指定为FruitAdapter.ViewHolder(ViewHolder为FruitAdapter中定义的内部类),如下:
public class FruitAdapter extends RecyclerView.Adapter {
private List mFruitList;
static class ViewHolder extends RecyclerView.ViewHolder{
ImageView fruitImage;
TextView fruitName;
public ViewHolder(View itemView) {//itemView参数通常时RecyclerView子项的最外层布局
super(itemView);
fruitImage = (ImageView) itemView.findViewById(R.id.fruit_image);
fruitName = (TextView) itemView.findViewById(R.id.fruit_name);
}
}
public FruitAdapter(List fruitList){//将要展示的数据源传递进来
mFruitList = fruitList;//赋值给全局变量
}
//由于继承自RecyclerView.Adapter,必须重写以下三个方法
@Override
public ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
//用于创建ViewHolder实例
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) {
//用于对RecyclerView子项的数据进行赋值的,在每个子项被滚动到屏幕内时执行,
// 通过position得到当前项的Fruit实例,再将数据设置到ViewHolder的ImageView和TextView中
Fruit fruit = mFruitList.get(position);
holder.fruitImage.setImageResource(fruit.getImageId());
holder.fruitName.setText(fruit.getName());
}
@Override
public int getItemCount() {//告诉RecyclerView一共有多少子项,返回数据源长度
return mFruitList.size();
}
}
使用RecyclerView:
public class MainActivity extends AppCompatActivity {
private List 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);//指定RecyclerView的布局方式
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 mango = new Fruit("Mango",R.drawable.mango_pic);
fruitList.add(mango);
}
}
}
这里实现了与前面ListView一样的效果。
(2)实现横向滚动和瀑布流布局
横向滚动,需要修改以下子项布局文件,改为垂直方向:
android:orientation="vertical"
android:layout_width="100dp"
android:layout_height="wrap_content">
android:layout_gravity="center_horizontal"/>
android:layout_gravity="center_horizontal"
android:layout_marginTop="10dp"/>
宽度设为定值,避免出现子项有长有短(文字长短不一)或宽度过长(match_parent)的问题影响美观。然后在主程序中加上如下代码即可:
LinearLayoutManager layoutManager = new LinearLayoutManager(this);//线性布局
layoutManager.setOrientation(LinearLayoutManager.HORIZONTAL);//设置为横向排列
recyclerView.setLayoutManager(layoutManager);//指定RecyclerView的布局方式
效果:
ListView的布局排列是由自身去管理的,而RecyclerView则交给LayoutManager(LayoutManager中制订了一套可扩展的布局排列接口,子类只要按照接口的规范来实现就能顶指出各种不同排列方式的布局了)。除了LinearLayout,还有GridLayoutManager和StaggeredGridLayoutManager(瀑布流)这两种内置的布局排列方式。
(2)瀑布流布局
修改子项布局文件:
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_margin="5dp">
android:layout_gravity="left"
android:layout_marginTop="10dp"/>
瀑布流布局的宽度应该是根据布局的列数自动调整的,不是固定值,子项之间需留一点间距。文本后面会通过代码拉长,所以改为居左对齐。
修改MainActivity代码:
StaggeredGridLayoutManager layoutManager = new StaggeredGridLayoutManager(3, StaggeredGridLayoutManager.VERTICAL);//两个参数:第一个用于指定布局列数(3列),第二个用于指定布局排列方式(纵向排列)
recyclerView.setLayoutManager(layoutManager);//指定RecyclerView的布局方式
修改文本内容长度(用random随机增加文本长度),让子项高度各不相同:
private String getRandomLengthName(String name){
Random random = new Random();
int length = random.nextInt(20) + 1;
StringBuilder buider = new StringBuilder();
for (int i = 0; i < length; i++){
buider.append(name);
}
return buider.toString();
}
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);
效果:
(3)点击事件
没有提供类似于setOnItemClickListener()(注册的是子项的点击事件,要点击子项里具体某一个按钮实现起来相对较麻烦,所以RecyclerView直接摒弃了这种监听器)的监听器,需自己给子项具体的View注册点击事件。 修改FruitAdapter中代码:
public class FruitAdapter extends RecyclerView.Adapter {
private List mFruitList;
static class ViewHolder extends RecyclerView.ViewHolder{
View fruitView;
//保存子项最外层布局的实例
//分别为最外层布局和ImageView注册点击事件
holder.fruitView.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
int position = holder.getAdapterPosition();//获取用户点击的position
Fruit fruit = mFruitList.get(position);
Toast.makeText(v.getContext(),"you clicked view" + fruit.getName(),Toast.LENGTH_SHORT).show();
}
});
holder.fruitImage.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
int position = holder.getAdapterPosition();
Fruit fruit = mFruitList.get(position);
Toast.makeText(v.getContext(),"you clicked image" + fruit.getName(),Toast.LENGTH_SHORT).show();
}
});
return holder;
}
效果如下(分别点击图片和文字):