Android的控件与布局

常用控件的使用方法

TextView

TextView是Android中最简单的一个控件

<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/textView"
 		android:layout_width="match_parent"
 		android:layout_height="wrap_content"
        android:gravity="center"
        android:textColor="#00ff00"
        android:textSize="24sp"
 		android:text="This is TextView"/>
</LinearLayout>

TextView标签内容
android:id:给当前控件定义了一个唯一标识符
android:layout_width、android:layout_height:指定了控件的宽度和高度
可选值有三种:

  • match_parent:让当前控件的大小和父布局的大小一样,也就是由父布局来决定当前控件的大小
  • wrap_content:让当前控件的大小能够刚好包含住里面的内容,也就是由控件内容决定当前控件的大小
  • 固定值:表示给控件指定一个固定的尺寸,单位一般用dp,这是一种屏幕密度无关的尺寸单位,可以保证在不同分辨率的手机上显示效果尽可能地一致,如50 dp就是一个有效的固定值
    android:gravity:指定文字的对齐方式,可选值有top、bottom、start、end、center等,可以用“|”来同时指定多个值
    android:textColor:指定文字的颜色
    android:textSize:指定文字的大小。文字大小要使用sp作为单位,这样当用户在系统中修改了文字显示尺寸时,应用程序中的文字大小也会跟着变化

运行效果:
image.png

Button

Button是程序用于和用户进行交互的一个重要控件,它可配置的属性和TextView是差不多的

<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/button"
 		android:layout_width="match_parent"
 		android:layout_height="wrap_content"
 		android:text="Button" />
</LinearLayout>

在MainActivity中为Button的点击事件注册一个监听器

override fun onCreate(savedInstanceState: Bundle?) {
 	super.onCreate(savedInstanceState)
 	setContentView(R.layout.activity_main)
 	button.setOnClickListener {
 		// 在此处添加逻辑
 	}
 }

调用button的setOnClickListener()方法时利用了Java单抽象方法接口的特性,从而可以使用函数式API的写法来监听按钮的点击事件。这样每当点击按钮时,就会执行Lambda表达式中的代码,我们只需要在Lambda表达式中添加待实现的逻辑就行了

使用函数式API的方式来注册监听器

class MainActivity : AppCompatActivity(), View.OnClickListener {
 	override fun onCreate(savedInstanceState: Bundle?) {
 		super.onCreate(savedInstanceState)
 		setContentView(R.layout.activity_main)
 		button.setOnClickListener(this)
 	}
 	override fun onClick(v: View?) {
 		when (v?.id) {
 			R.id.button -> {
 				// 在此处添加逻辑
 			}
 		}
 	}
}

让MainActivity实现了View.OnClickListener接口,并重写了onClick()方法,然后在调用button的setOnClickListener()方法时将MainActivity的实例传了进去。这样每当点击按钮时,就会执行onClick()方法中的代码了

运行效果:
image.png

EditText

EditText是程序用于和用户进行交互的另一个重要控件,它允许用户在控件里输入和编辑内容,并可以在程序中对这些内容进行处理

<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
 	android:orientation="vertical"
 	android:layout_width="match_parent"
 	android:layout_height="match_parent">
 
 	<EditText
 		android:id="@+id/editText"
 		android:layout_width="match_parent"
 		android:layout_height="wrap_content"
 		android:hint="Type something here"
 		android:maxLines="2"/>
</LinearLayout>

android:hint:指定了一段提示性的文本
android:maxLines:指定了EditText的最大行数为两行,这样当输入的内容超过两行时,文本就会向上滚动,EditText则不会再继续拉伸

运行效果:
image.png

通过点击按钮获取EditText中输入的内容

class MainActivity : AppCompatActivity(), View.OnClickListener {
 	...
 	override fun onClick(v: View?) {
 		when (v?.id) {
 			R.id.button -> {
 				val inputText = editText.text.toString()
 				Toast.makeText(this, inputText, Toast.LENGTH_SHORT).show()
 			}
 		}
 	}
}

在按钮的点击事件里调用EditText的getText()方法获取输入的内容,再调用toString()方法将内容转换成字符串,最后使用Toast将输入的内容显示出来

ImageView

ImageView是用于在界面上展示图片的一个控件,它可以让程序界面变得更加丰富多彩

<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
 	android:orientation="vertical"
 	android:layout_width="match_parent"
 	android:layout_height="match_parent">
 
 	<ImageView
 		android:id="@+id/imageView"
 		android:layout_width="wrap_content"
 		android:layout_height="wrap_content"
 		android:src="@drawable/img_1"/>
</LinearLayout>

使用android:src属性给ImageView指定了一张图片。由于图片的宽和高都是未知的,所以将ImageView的宽和高都设定为wrap_content,这样就保证了不管图片的尺寸是多少,都可以完整地展示出来

运行效果
image.png

动态地更改ImageView中的图片

class MainActivity : AppCompatActivity(), View.OnClickListener {
 	...
 	override fun onClick(v: View?) {
 		when (v?.id) {
 			R.id.button -> {
 				imageView.setImageResource(R.drawable.img_2)
 			}
 		}
 	}
}

在按钮的点击事件里,通过调用ImageView的setImageResource()方法将显示的图片改成img_2

运行效果
image.png

ProgressBar

ProgressBar用于在界面上显示一个进度条,表示我们的程序正在加载一些数据

<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
 	android:orientation="vertical"
 	android:layout_width="match_parent"
 	android:layout_height="match_parent">

 	<ProgressBar
 		android:id="@+id/progressBar"
		android:layout_width="match_parent"
 		android:layout_height="wrap_content"
    	style="?android:attr/progressBarStyleHorizontal"
        android:max="100"/>
</LinearLayout>

style:可以将进度条指定成水平进度条
android:max:给进度条设置一个最大值
android:visibility:

  • visible:表示控件是可见的,这个值是默认值,不指定android:visibility时,控件都是可见的
  • invisible:表示控件不可见,但是它仍然占据着原来的位置和大小,可以理解成控件变成透明状态了
  • gone:则表示控件不仅不可见,而且不再占用任何屏幕空间

可以通过代码来设置控件的可见性,使用的是setVisibility()方法,允许传入View.VISIBLE、View.INVISIBLE和View.GONE这3种值。

运行效果
image.png

另一用法:
点击一下按钮让进度条消失,再点击一下按钮让进度条出现

class MainActivity : AppCompatActivity(), View.OnClickListener {
 	...
 	override fun onClick(v: View?) {
 		when (v?.id) {
 			R.id.button -> {
 				if (progressBar.visibility == View.VISIBLE) {
 					progressBar.visibility = View.GONE
 				} else {
 					progressBar.visibility = View.VISIBLE
 				}
 			}
 		}
 	}
}

在按钮的点击事件中,通过getVisibility()方法来判断ProgressBar是否可见,如果可见就将ProgressBar隐藏掉,如果不可见就将ProgressBar显示出来
image.png

AlertDialog

AlertDialog可以在当前界面弹出一个对话框,这个对话框是置顶于所有界面元素之上的,能够屏蔽其他控件的交互能力,因此AlertDialog一般用于提示一些非常重要的内容或者警告信息。比如为了防止用户误删重要内容,在删除前弹出一个确认对话框

class MainActivity : AppCompatActivity(), View.OnClickListener {
 	...
 	override fun onClick(v: View?) {
 		when (v?.id) {
 			R.id.button -> {
 				AlertDialog.Builder(this).apply {
 					setTitle("This is Dialog")
 					setMessage("Something important.")
 					setCancelable(false)
 					setPositiveButton("OK") { dialog, which ->}
 					setNegativeButton("Cancel") { dialog, which ->}
					show()
 				}
 			}
 		}
 	}
}

首先通过AlertDialog.Builder构建一个对话框
使用Kotlin标准函数中的apply函数。在apply函数中为这个对话框设置标题、内容、可否使用Back键关闭对话框等属性
接下来调用setPositiveButton()方法为对话框设置确定按钮的点击事件
调用setNegativeButton()方法设置取消按钮的点击事件
最后调用show()方法将对话框显示出来就可以了

运行效果
image.png

CheckBox

这是一个复选框控件,用户可以通过点击的方式进行选中和取消

<CheckBox
 android:id="@+id/rememberPass"
 android:layout_width="wrap_content"
 android:layout_height="wrap_content" />

ScrollView

由于手机屏幕的空间一般比较小,有些时候过多的内容一屏是显示不下的,借助ScrollView控件,就可以以滚动的形式查看屏幕外的内容

基本布局

LinearLayout

LinearLayout又称作线性布局,是一种非常常用的布局。正如它的名字所描述的一样,这个布局会将它所包含的控件在线性方向上依次排列
android:orientation:

  • vertical:控件在垂直方向排列
  • horizontal:控件在水平方向上排列,默认值

注意:
如果LinearLayout的排列方向是horizontal,内部的控件就绝对不能将宽度指定为match_parent,否则,单独一个控件就会将整个水平方向占满,其他的控件就没有可放置的位置了。同样的道理,如果LinearLayout的排列方向是vertical,内部的控件就不能将高度指定为match_parent。

<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>

android:layout_gravity:用于指定控件在布局中的对齐方式

  • top:置顶
  • center_vertical:置中
  • bottom:置尾

注意:
当LinearLayout的排列方向是horizontal时,只有垂直方向上的对齐方式才会生效。因为此时水平方向上的长度是不固定的,每添加一个控件,水平方向上的长度都会改变,因而无法指定该方向上的对齐方式。同样的道理,当LinearLayout的排列方向是vertical时,只有水平方向上的对齐方式才会生效

<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 something"/>
     <Button
         android:id="@+id/send"
         android:layout_width="0dp"
         android:layout_height="wrap_content"
         android:layout_weight="1"
         android:text="Send"/>
</LinearLayout>

android:layout_weight:允许使用比例的方式来指定控件的大小
由于使用了android:layout_weight属性,此时控件的宽度就不应该再由android:layout_width来决定了,这里指定成0 dp是一种比较规范的写法
系统会先把LinearLayout下所有控件指定的layout_weight值相加,得到一个总值,然后每个控件所占大小的比例就是用该控件的layout_weight值除以刚才算出的总值

RelativeLayout

RelativeLayout又称作相对布局,也是一种非常常用的布局。和LinearLayout的排列规则不同,RelativeLayout显得更加随意,它可以通过相对定位的方式让控件出现在布局的任何位置

与父控件对齐:
android:layout_alignParentTop:如果为true,将该控件的顶部与其父控件的底部对齐
android:layout_alignParentBottom:如果为true,将该控件的底部与其父控件的底部对齐
android:layout_alignParentLeft:如果为true,将该控件的左部与其父控件的左部对齐
android:layout_alignParentRight:如果为true,将该控件的右部与其父控件的右部对齐
android:layout_centerHorizontal: 如果为true,将该控件水平居中
android:layout_centerVertical:如果为true,将该控件垂直居中
android:layout_centerInParent:如果为true,将该控件置于父控件的中央
上面属性中的每个控件都是相对于父布局进行定位的

边缘对齐但不会覆盖:
android:layout_above:属性可以让一个控件位于另一个控件的上方
android: layout_below:表示让一个控件位于另一个控件的下方
android:layout_toLeftOf:表示让一个控件位于另一个控件的左侧
android:layout_toRightOf:表示让一个控件位于另一个控件的右侧
上面属性中的每个控件都是相对于控件进行定位

边缘对齐但是有可能覆盖:
android:layout_alignLeft:表示让一个控件的左边缘和另一个控件的左边缘对齐android:layout_alignRight:表示让一个控件的右边缘和另一个控件的右边缘对齐
android:layout_alignTop:表示让一个控件的上边缘和另一个控件的上边缘对齐
android:layout_alignBottom:表示让一个控件的下边缘和另一个控件的下边缘对齐

FrameLayout

FrameLayout又称作帧布局,它相比于前面两种布局简单太多。这种布局没有丰富的定位方式,所有的控件都会默认摆放在布局的左上角
layout_gravity:
left:左对齐
right:右对齐
top:上对齐
bottom:下对齐

特殊控件

自定义控件

新建Activity继承LinearLayout,这样就可以成为自定义的标题栏控件

class TitleLayout(context: Context, attrs: AttributeSet) : LinearLayout(context, attrs) {
 	init {
 		LayoutInflater.from(context).inflate(R.layout.title, this)
 	}
}

在主构造函数中声明了Context和AttributeSet这两个参数,在布局中引入TitleLayout控件时就会调用这个构造函数。
在init结构体中需要对标题栏布局进行动态加载,需要借助LayoutInflflater来实现
通过LayoutInflflater的from()方法可以构建出一个LayoutInflater对象,然后调用inflate()方法就可以动态加载一个布局文件
inflate()方法接收两个参数:第一个参数是要加载的布局文件的id,这里我们传入R.layout.title;第二个参数是给加载好的布局再添加一个父布局,这里要指定为TitleLayout,于是直接传入this

在布局文件中添加这个自定义控件,添加自定义控件和添加普通控件的方式基本是一样的,只不过在添自定义控件的时候,需要指明控件的完整类名,包名在这里是不可以省略的

<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
     android:layout_width="match_parent"
     android:layout_height="match_parent" >
     <com.example.uicustomviews.TitleLayout
         android:layout_width="match_parent"
         android:layout_height="wrap_content" />
</LinearLayout>

最常用和最难用的控件:ListView

ListView允许用户通过手指上下滑动的方式将屏幕外的数据滚动到屏幕内,同时屏幕上原有的数据会滚动出屏幕。你其实每天都在使用这个控件,比如查看QQ聊天记录,翻阅微博最新消息,等等
加入ListView控件和之前的类似,先为ListView指定一个id,然后将宽度和高度都设置为match_parent
修改MainActivity中的代码:

class MainActivity : AppCompatActivity() {
     private val data = listOf("Apple", "Banana", "Orange", "Watermelon",
         "Pear", "Grape", "Pineapple", "Strawberry", "Cherry", "Mango",
         "Apple", "Banana", "Orange", "Watermelon", "Pear", "Grape",
         "Pineapple", "Strawberry", "Cherry", "Mango")
     
     override fun onCreate(savedInstanceState: Bundle?) {
         super.onCreate(savedInstanceState)
         setContentView(R.layout.activity_main)
         val adapter = ArrayAdapter<String>(this,android.R.layout.simple_list_item_1,data)
         listView.adapter = adapter
     }
}

集合中的数据是无法直接传递给ListView,需要借助适配器来完成,这里使用ArrayAdapter适配器
ArrayAdapter有多个构造函数的重载,根据实际情况选择最合适的一种,这里指定String
在ArrayAdapter的构造函数中依次传入Activity的实例、ListView子项布局的id,以及数据源

android.R.layout.simple_list_item_1:是ListView子项布局的id,这是一个Android内置的布局文件,里面只有一个TextView,可用于简单地显示一段文本

例:
实体类:

class Fruit(val name:String, val imageId: Int)

为ListView的子项指定一个自定义的布局:

<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
     android:layout_width="match_parent"
     android:layout_height="60dp">
     <ImageView
         android:id="@+id/fruitImage"
         android:layout_width="40dp"
         android:layout_height="40dp"
         android:layout_gravity="center_vertical"
         android:layout_marginLeft="10dp"/>
     <TextView
         android:id="@+id/fruitName"
         android:layout_width="wrap_content"
         android:layout_height="wrap_content"
         android:layout_gravity="center_vertical"
         android:layout_marginLeft="10dp" />
</LinearLayout>

定义了一个ImageView用于显示水果的图片,又定义了一个TextView用于显示水果的名称
让ImageView和TextView都在垂直方向上居中显示

创建一个自定义的适配器,这个适配器继承自ArrayAdapter,并将泛型指定为Fruit类:

class FruitAdapter(activity: Activity, val resourceId: Int, data: List<Fruit>) :
    ArrayAdapter<Fruit>(activity, resourceId, data) {

    inner class ViewHolder(val fruitImage : ImageView, val fruitName : TextView)

    override fun getView(position: Int, convertView: View?, parent: ViewGroup): View {
        val view: View
        val viewHolder : ViewHolder
        if (convertView == null) {
            view = LayoutInflater.from(context).inflate(resourceId, parent, false)
            val fruitImage: ImageView = view.findViewById(R.id.fruitImage)
            val fruitName: TextView = view.findViewById(R.id.fruitName)
            viewHolder = ViewHolder(fruitImage,fruitName)
            view.tag = viewHolder
        } else {
            view = convertView
            viewHolder = view.tag as ViewHolder
        }
        
        val fruit = getItem(position) // 获取当前项的Fruit实例
        if (fruit != null) {
            viewHolder.fruitImage.setImageResource(fruit.imageId)
            viewHolder.fruitName.text = fruit.name
        }
        return view
    }
}

FruitAdapter定义了一个主构造函数,用于将Activity的实例、ListView子项布局的id和数据源传递进来
重写getView()方法,这个方法在每个子项被滚动到屏幕内的时候会被调用。
LayoutInflater的inflate()方法接收3个参数:ListView子项布局的id、控件、第三个表示只让我们在父布局中声明的layout属性生效,但不会为这个View添加父布局
通过getItem()方法得到当前项的Fruit实例,并分别调用它的setImageResource()和setText()方法设置显示的图片和文字,最后将布局返回

提升ListView的运行效率:
增加内部类ViewHolder,用于对ImageView和TextView的控件实例进行缓存
在getView()方法中进行了判断:
当convertView为null:使用LayoutInflater去加载布局;并且创建一个ViewHolder对象,将控件的实例存放在ViewHolder里,然后调用View的setTag()方法,将ViewHolder对象存储在View中
当convertView不为null:则直接对convertView进行重用;然后调用View的getTag()方法,把ViewHolder重新取出

修改MainActivity

class MainActivity : AppCompatActivity() {
    
    private val fruitList = ArrayList<Fruit>()
     
    override fun onCreate(savedInstanceState: Bundle?) {
         super.onCreate(savedInstanceState)
         setContentView(R.layout.activity_main)
         initFruits() // 初始化水果数据
         val adapter = FruitAdapter(this, R.layout.fruit_item, fruitList)
         listView.adapter = adapter
         listView.setOnItemClickListener { _, _, position, _ ->
             val fruit = fruitList[position]
             Toast.makeText(this, fruit.name, Toast.LENGTH_SHORT).show()
         }
     }
     
     private fun initFruits() {
         repeat(2) {
             fruitList.add(Fruit("Apple", R.drawable.apple_pic))
             fruitList.add(Fruit("Banana", R.drawable.banana_pic))
             fruitList.add(Fruit("Orange", R.drawable.orange_pic))
             fruitList.add(Fruit("Watermelon", R.drawable.watermelon_pic))
             fruitList.add(Fruit("Pear", R.drawable.pear_pic))
             fruitList.add(Fruit("Grape", R.drawable.grape_pic))
             fruitList.add(Fruit("Pineapple", R.drawable.pineapple_pic))
             fruitList.add(Fruit("Strawberry", R.drawable.strawberry_pic))
             fruitList.add(Fruit("Cherry", R.drawable.cherry_pic))
             fruitList.add(Fruit("Mango", R.drawable.mango_pic))
         }
 	 }
}

initFruits()方法,用于初始化所有的水果数据
在Fruit类的构造函数中将水果的名字和对应的图片id传入,然后把创建好的对象添加到水果列表中
repeat函数:是Kotlin中常用的标准函数,它允许你传入一个数值_n_,然后会把Lambda表达式中的内容执行_n_遍
在onCreate()方法中创建了FruitAdapter对象,并将它作为适配器传递给ListView

增加ListView的点击事件:
使用setOnItemClickListener()方法为ListView注册了一个监听器
当用户点击了ListView中的任何一个子项时,就会回调到Lambda表达式中
通过position参数判断用户点击的是哪一个子项,然后获取到相应的水果,并通过Toast将水果的名字显示出来
在Lambda表达式中声明4个参数,但实际上却只用到了position这一个参数而已。针对这种情况,可以将没有用到的参数使用下划线(_)来替,不过参数之间的位置不能改变
:::

更强大的滚动控件:RecyclerView

RecyclerView:是一个增强版的ListView,不仅可以轻松实现和ListView同样的效果,还优化了ListView存在的各种不足之处
添加RecyclerView库的依赖:

implementation 'androidx.recyclerview:recyclerview:1.2.1

加入RecyclerView控件:

<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/recyclerView"
         android:layout_width="match_parent"
         android:layout_height="match_parent" />
</LinearLayout>

RecyclerView指定一个id
宽度和高度都设置为match_parent,这样RecyclerView就占满了整个布局的空间
由于RecyclerView并不是内置在系统SDK当中的,所以需要把完整的包路径写出来

为RecyclerView准备一个适配器,这个适配器继承自RecyclerView.Adapter,并将泛型指定为FruitAdapter.ViewHolder:

class FruitAdapter(val fruitList: List<Fruit>) :
         RecyclerView.Adapter<FruitAdapter.ViewHolder>() {
             
     inner class ViewHolder(view: View) : RecyclerView.ViewHolder(view) {
         val fruitImage: ImageView = view.findViewById(R.id.fruitImage)
         val fruitName: TextView = view.findViewById(R.id.fruitName)
     }
     
     override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
         val view = LayoutInflater.from(parent.context)
         .inflate(R.layout.fruit_item, parent, false)
         return ViewHolder(view)
     }
     
     override fun onBindViewHolder(holder: ViewHolder, position: Int) {
         val fruit = fruitList[position]
         holder.fruitImage.setImageResource(fruit.imageId)
         holder.fruitName.text = fruit.name
     }
     
     override fun getItemCount() = fruitList.size
             
}

FruitAdapter中主构造函数,它用于把要展示的数据源传进来
定义一个内部类ViewHolder,继承RecyclerView.ViewHolder;ViewHolder的主构造函数中要传入一个View参数,这个参数通常就是RecyclerView子项的最外层布局
通过findViewById()方法来获取布局中ImageView和TextView的实例了

FruitAdapter是继承自RecyclerView.Adapter的,那么就必须重写onCreateViewHolder()、onBindViewHolder()和getItemCount()这3个方法
onCreateViewHolder():用于创建ViewHolder实例的,在这个方法中将fruit_item布局加载进来,然后创建一个ViewHolder实例,并把加载出来的布局传入构造函数当中,最后将ViewHolder的实例返回
onBindViewHolder():用于对RecyclerView子项的数据进行赋值,会在每个子项被滚动到屏幕内的时候执行,这里通过position参数得到当前项的Fruit实例,然后再将数据设置到ViewHolder的ImageView和TextView当中即可
getItemCount():用于告诉RecyclerView一共有多少子项,直接返回数据源的长度

修改MainActivity:

class MainActivity : AppCompatActivity() {
    
     private val fruitList = ArrayList<Fruit>()
     
     override fun onCreate(savedInstanceState: Bundle?) {
         super.onCreate(savedInstanceState)
         setContentView(R.layout.activity_main)
         initFruits() // 初始化水果数据
         val layoutManager = LinearLayoutManager(this)
         recyclerView.layoutManager = layoutManager
         val adapter = FruitAdapter(fruitList)
         recyclerView.adapter = adapter
     }
     
     private fun initFruits() {
         repeat(2) {
             fruitList.add(Fruit("Apple", R.drawable.apple_pic))
             fruitList.add(Fruit("Banana", R.drawable.banana_pic))
             fruitList.add(Fruit("Orange", R.drawable.orange_pic))
             fruitList.add(Fruit("Watermelon", R.drawable.watermelon_pic))
             fruitList.add(Fruit("Pear", R.drawable.pear_pic))
             fruitList.add(Fruit("Grape", R.drawable.grape_pic))
             fruitList.add(Fruit("Pineapple", R.drawable.pineapple_pic))
             fruitList.add(Fruit("Strawberry", R.drawable.strawberry_pic))
             fruitList.add(Fruit("Cherry", R.drawable.cherry_pic))
             fruitList.add(Fruit("Mango", R.drawable.mango_pic))
         }
     }
     
}

使用了一个initFruits()方法,用于初始化所有的水果数据
onCreate()方法中先创建了一个LinearLayoutManager对象,并将它设置到RecyclerView当中
LayoutManager用于指定RecyclerView的布局方式,这里使用的LinearLayoutManager是线性布局,可以实现和ListView类似的效果
创建FruitAdapter实例,并将水果数据传入FruitAdapter的构造函数中
调用RecyclerView的setAdapter()方法来完成适配器设置

LayoutManager制定了一套可扩展的布局排列接口,子类只要按照接口的规范来实现,就能定制出各种不同排列方式的布局
布局横行排列:
调用LinearLayoutManager的setOrientation()方法设置布局的排列方向
默认是纵向排列的,传入LinearLayoutManager.HORIZONTAL表示让布局横行排列,这样RecyclerView就可以横向滚动了
瀑布流布局:
创建一个StaggeredGridLayoutManager实例
StaggeredGridLayoutManager的构造函数接收两个参数:
第一个参数用于指定布局的列数,传入3表示会把布局分为3列
第二个参数用于指定布局的排列方向,传入StaggeredGridLayoutManager.VERTICAL表示会让布局纵向排列

增加点击事件:修改FruitAdapter中的onCreateViewHolder()方法代码

override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
        val view = LayoutInflater.from(parent.context)
        .inflate(R.layout.fruit_item, parent, false)
        val viewHolder = ViewHolder(view)
        viewHolder.itemView.setOnClickListener {
            val position = viewHolder.adapterPosition
            val fruit = fruitList[position]
            Toast.makeText(parent.context, "you clicked view ${fruit.name}",
                Toast.LENGTH_SHORT).show()
        }
        viewHolder.fruitImage.setOnClickListener {
            val position = viewHolder.adapterPosition
            val fruit = fruitList[position]
            Toast.makeText(parent.context, "you clicked image ${fruit.name}",
                Toast.LENGTH_SHORT).show()
        }
        return viewHolder
}

分别为最外层布局和ImageView都注册了点击事件,itemView表示的就是最外层布局
在两个点击事 件中先获取了用户点击的position,通过position拿到相应的Fruit实例
使用Toast分别弹出两种不同的内容以示区别

编写界面的最佳实践

编写主界面,修改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:id="@+id/recyclerView"
         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/inputText"
         android:layout_width="0dp"
         android:layout_height="wrap_content"
         android:layout_weight="1"
         android:hint="Type something here"
         android:maxLines="2" />
    
     <Button
         android:id="@+id/send"
         android:layout_width="wrap_content"
         android:layout_height="wrap_content"
         android:text="Send" />
    
     </LinearLayout>
    
</LinearLayout>

创建一个RecyclerView用于显示聊天的消息内容
创建一个EditText用于输入消息,还创建一个Button用于发送消息

定义消息的实体类,新建Msg:

class Msg(val content: String, val type: Int) {
     companion object {
         const val TYPE_RECEIVED = 0
         const val TYPE_SENT = 1
     }
}

content表示消息的内容,type表示消息的类型
消息类型有两个值可选:TYPE_RECEIVED表示这是一条收到的消息,TYPE_SENT表示这是一条发出的消息

编写RecyclerView的子项布局,新建msg_left_item.xml:

<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
     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:layout_gravity="left"
         android:background="@drawable/message_left" >
    
     <TextView
         android:id="@+id/leftMsg"
         android:layout_width="wrap_content"
         android:layout_height="wrap_content"
         android:layout_gravity="center"
         android:layout_margin="10dp"
         android:textColor="#fff" />
    
     </LinearLayout>
    
</FrameLayout>

这是接收消息的子项布局。让收到的消息居左对齐

新建msg_right_item.xml:

<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
     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:layout_gravity="right"
         android:background="@drawable/message_right" >
    
     <TextView
         android:id="@+id/rightMsg"
         android:layout_width="wrap_content"
         android:layout_height="wrap_content"
         android:layout_gravity="center"
         android:layout_margin="10dp"
         android:textColor="#000" />
    
     </LinearLayout>
    
</FrameLayout>

这是发送消息的子项布局。让发出的消息居右对齐

创建RecyclerView的适配器类,新建类MsgAdapter:

class MsgAdapter(val msgList: List<Msg>) : RecyclerView.Adapter<RecyclerView.ViewHolder>() {
     
    inner class LeftViewHolder(view: View) : RecyclerView.ViewHolder(view) {
     	val leftMsg: TextView = view.findViewById(R.id.leftMsg)
     }
     
     inner class RightViewHolder(view: View) : RecyclerView.ViewHolder(view) {
     	val rightMsg: TextView = view.findViewById(R.id.rightMsg)
     }
     
     override fun getItemViewType(position: Int): Int {
         val msg = msgList[position]
         return msg.type
     }
     
     override fun onCreateViewHolder(parent: ViewGroup, viewType: Int) = if (viewType ==
     			Msg.TYPE_RECEIVED) {
     	val view = LayoutInflater.from(parent.context).inflate(R.layout.msg_left_item,
     				parent, false)
     	LeftViewHolder(view)
     } else {
     	val view = LayoutInflater.from(parent.context).inflate(R.layout.msg_right_item,
     				parent, false)
     	RightViewHolder(view)
     }
     
     override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) {
         val msg = msgList[position]
         when (holder) {
             is LeftViewHolder -> holder.leftMsg.text = msg.content
             is RightViewHolder -> holder.rightMsg.text = msg.content
             else -> throw IllegalArgumentException()
         }
     }
     
     override fun getItemCount() = msgList.size
}

首先定义了LeftViewHolder和RightViewHolder这两个ViewHolder,分别用于缓存 msg_left_item.xml和msg_right_item.xml布局中的控件
然后重写getItemViewType()方法,并在这个方法中返回当前position对应的消息类型

在onCreateViewHolder()方法中根据不同的viewType来加载不同的布局并创建不同的ViewHolder
然后在onBindViewHolder()方法中判断ViewHolder的类型:
如果是LeftViewHolder,就将内容显示到左边的消息布局
如果是RightViewHolder,就将内容显示到右边的消息布局

修改MainActivity中的代码,为RecyclerView初始化一些数据,并给发送按钮加入事件响应

class MainActivity : AppCompatActivity(), View.OnClickListener {

    private val msgList = ArrayList<Msg>()

    private var adapter: MsgAdapter? = null

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)
        initMsg()
        val layoutManager = LinearLayoutManager(this)
        val recyclerView : RecyclerView = findViewById(R.id.recyclerView)
        recyclerView.layoutManager = layoutManager
        adapter = MsgAdapter(msgList)
        recyclerView.adapter = adapter
        val send : Button = findViewById(R.id.send)
        send.setOnClickListener(this)
    }

    override fun onClick(v: View?) {
        val send : Button = findViewById(R.id.send)
        val recyclerView : RecyclerView = findViewById(R.id.recyclerView)
        val inputText : EditText = findViewById(R.id.inputText)
        when (v) {
            send -> {
                val content = inputText.text.toString()
                if (content.isNotEmpty()) {
                    val msg = Msg(content, Msg.TYPE_SENT)
                    msgList.add(msg)
                    adapter?.notifyItemInserted(msgList.size - 1) // 当有新消息时,刷新RecyclerView中的显示
                    recyclerView.scrollToPosition(msgList.size - 1) // 将RecyclerView 定位到最后一行
                    inputText.setText("") // 清空输入框中的内容
                }
            }
        }
    }

    private fun initMsg() {
        val msg1 = Msg("Hello guy.", Msg.TYPE_RECEIVED)
        msgList.add(msg1)
        val msg2 = Msg("Hello. Who is that?", Msg.TYPE_SENT)
        msgList.add(msg2)
        val msg3 = Msg("This is Tom. Nice talking to you. ", Msg.TYPE_RECEIVED)
        msgList.add(msg3)
    }
}

在initMsg()方法中初始化了几条数据用于在RecyclerView中显示
在发送按钮的点击事件里获取了EditText中的内容,如果内容不为空字符串,则创建一个新的Msg对象并添加到msgList列表中去
调用适配器的notifyItemInserted()方法,用于通知列表有新的数据插入,这样新增的一条消息才能够在RecyclerView中显示出来。或者你也可以调用适配器的notifyDataSetChanged()方法,它会将RecyclerView中所有可见的元素全部刷新,这样不管是新增、删除、还是修改元素,界面上都会显示最新的数据,但缺点是效率会相对差一些
调用RecyclerView的scrollToPosition()方法将显示的数据定位到最后一行,以保证一定可以看得到最后发出的一条消息
调用EditText的setText()方法将输入的内容清空

运行程序:
image.png

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值