常见控件的使用方法
TextView
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".MainActivity">
<TextView
android:id="@+id/textView"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="This is TextView"
/>
</LinearLayout>
android:id 给控件指定唯一的标识符
android:layout_width 和android:layout_heigh 指定控件的宽度和高度。Android所有的控件都具有这两个属性,可选值有三种 ,match_parent,wrap_content和固定值。match_parent,表示让当前控件的大小和父布局的大小一样大。wrap_content表示让当前控件的大小能够刚好包含住里面的内容,也就是由控件内容决定当前控件的大小。固定值表示给控件指定一个固定的尺寸,单位一般是dp,这是一种与屏幕密度无关的尺寸单位,可以保证在不同分辨率的手机上显示效果尽可能的一致。
在上面的布局中,最后的显示效果没感觉出TextView的宽度和屏幕一样长是因为,TextView中的文字是默认居左上角对齐的,所以TextView的宽度充满了整个屏幕,但文字内容不长,所以视觉效果看不出来。
android:gravity 可以指定文字的对齐方式,可选值有top,bottom,start,end,center等。可以用 | 来指定多个值,用center 效果等同于center_vertical|center_horizontal 表示文字在垂直和水平方向都居中对齐。 将 android:gravity="center"加入layout布局,发现文字居屏幕中间显示。根据center的原理可知,说明TextView此时的宽度的确是屏幕宽度
android:textColor 可以指定文字颜色,android:textSize指定文字大小。 文字大小要使用sp作为单位,这样当用户在系统中修改了文字显示尺寸时,应用程序中的文字大小也会跟着变化。
Button
Button是程序用于和用户进行交互的一个重要控件。
<Button
android:id="@+id/button"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="Button"/>
这里我们在XML中指定按钮的文字是Button,但实际显示是BUTTON。这是因为Android系统默认会将按钮上的英文字母全部转换成大写,可能认为按钮上的重要很重要吧。如果想要保留指定的原始文字内容,可以在XML中添加android:textAllCaps="false"这个属性
class MainActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
button.setOnClickListener {
}
}
}
这里调用了button的setOnClickListener()方法时 利用了Java的单抽象方法接口的特性,从而可以使用函数式API的写法来监听按钮的点击事件。这样每点击按钮时,就会执行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()方法了。 注意:这里一定要调用button的setOnClickListener()方法将MainActivity的实例传进去,否则点击无效。点击事件比较多时,实现接口的方式明显更加整洁,美观。当点击事件较少时,通过函数式API的方式注册监听器,更加快捷。
class MainActivity : AppCompatActivity(), View.OnClickListener {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
button.setOnClickListener(this)
button2.setOnClickListener(this)
textView.setOnClickListener(this)
}
override fun onClick(v: View?) {
when(v?.id)
{
R.id.button ->{
Toast.makeText(this,"Butoon is cliced",Toast.LENGTH_SHORT).show()
}
R.id.button2->
{
Toast.makeText(this,"Butoon2 is cliced",Toast.LENGTH_SHORT).show()
}
R.id.textView->
{
Toast.makeText(this,"textView is cliced",Toast.LENGTH_SHORT).show()
}
}
}
}
EditText
EditText是程序和用户交互的重要控件,它允许用户在控件里输入和编辑内容,并且可以在程序中对这些内容进行处理。
<EditText
android:id="@+id/editText"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:hint="please input"
android:maxLines="2"/>
android:hint 属性可以让编辑框显示提示性文字
由于EditText的高度指定为wrap_content,总能包住里面的内容,但当输入的内容过多时,界面就会很难看。这里使用android:maxLines 属性来解决这个问题。 这样当输入的内容超过最大行数时,文本就会向上滚动,EditText则不会再继续拉伸。
EditText结合Button可以完成一些功能,比如通过点击按钮EditText中输入的内容。
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显示。
当然,上面代码使用了Kotlin调用了Java Getter和Setter方法的语法糖,代码中好像是调用了EditText的text属性,实际上却是EditText的getText()方法。如果在编写代码时,键入editText.getText()方法,代码提示将它转换成text,即Andorid Studio会自动在代码提示中显示使用语法糖后的优化代码调用。
Image View
ImageView是用于在界面上展示图片的一个控件,它可以让我们的程序变得更加丰富多彩。 图片通常都是放在以drawable开头的目录下的,并且要加上具体的分辨率。现在主流的手机屏幕的分辨率大多是xxhdpi的,所以我们一般在res目录下再新建一个drawable-xxhdpi目录。
<ImageView
android:id="@+id/imageView"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:src="@drawable/img_1"/>
使用android:src 属性给ImageView指定了一张图片。由于图片的宽度和高度未知,所以将ImageView的宽和高都设置为wrap_content,这样保证了不管图片的尺寸是多少,都可以完整地展示。
在程序中也可以动态地更改ImageView的图片
R.id.button ->{
imageView.setImageResource(R.drawable.img_2)
}
在按钮的点击事件里,通过调用ImageView的setImageResource()方法将显示的图片改变。
ProgressBar
ProgressBar用于在界面上显示一个进度条,表示我们的程序正在加载一些数据。
<ProgressBar
android:id="@+id/progressBar"
android:layout_width="match_parent"
android:layout_height="wrap_content"/>
旋转的进度条表示程序正在加载数据,数据总有加载完的时候,那如何让进度条在数据加载完成时消失呢? 这里需要一个新的知识点,Android控件的可见属性。所有Android控件都具有这个属性,可以通过android:visibility进行指定,可选值有3种,visible,invisible和gone。 visible表示控件是可见的,这个值是默认值,不指定android:visibility时,控件都是可见的。invisible表示控件是不可见,但是它仍然占据原来的位置和大小,可以理解控件变成透明状态了。gone则表示控件不仅不可见,而且不再占据任何屏幕空间。 我们也可以通过代码设置控件的可见性,使用的是setVisibility()方法,允许传入View.VISIBLE,View.INVISIBLE,View.GONE这三个值。 ( 注意:控件被设置为invisible时,控件变成了透明状态,此时控件会占据原理的位置和空间,但控件无法被选定,比如设置了监听器的button,将无法被点击。)
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显示。
通过style属性可以给ProgressBar指定不同的样式
<ProgressBar
android:id="@+id/progressBar"
android:layout_width="match_parent"
android:layout_height="wrap_content"
style="?android:attr/progressBarStyleHorizontal"
android:max="100"
/>
指定成水平进度条后,可以通过android:max属性给进度条设置一个最大值,然后在代码中动态地更改进度条的进度。
override fun onClick(v: View?) {
when(v?.id)
{
R.id.button ->{
progressBar.progress=progressBar.progress+10
if(progressBar.progress==100)
progressBar.visibility=View.GONE
}
}
}
每点击一次按钮,我们就获取进度条当前的进度,然后在现有的进度加上10,作为更新后的进度。直到进度条达到之前设定的最大值100,我们设置隐藏进度条。 注意:这里设置的点击监听器已经是监测了每次点击后的监听情况。所以没必要在逻辑里面再循环条件判断进度条是否进度满100了。
AlertDialog
AlertDialog可以在当前界面弹出一个对话框,这个对话框置顶于所有界面元素之上,能够屏蔽其他控件的交互能力,因此AlertDialog一般用于提示一些非常重要的内容或者警告信息。比如放置用户误删一些重要内容,在删除前弹出一个确认对话框。
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构建了一个对话框,这里使用apply函数,括号里面的this,声明上下文是默认的MainActivity。调用了AlertDialog.Builder的对象,给对话框设置标题,内容,可否使用Back键关闭对话框等属性,调用 setPositiveButton()方法为对话框设置确定按钮的点击事件,调用 setNegativeButton()为对话框设置取消按钮的点击事件,最后调用show()方法将对话框显示出来。
详解3种基本布局
布局是一种可以放置很多控件的容器,它可以按照一定的规律调整内部控件的位置,从而编写出精美的界面。当然布局的内部除了放置控件,也可以放置布局,通过多层布局的签到,完成复杂界面的实现。
LinearLayout
LinearLayout又称线性布局,这个布局会将它所含的控件在线性方向依次排列。既然是线性布局,那么就肯定不止一个方向。通过android:orientation属性指定排列方向,vertical是垂直方向,horizontal是水平方向。(不指定android:orientation属性的值的话,默认是水平方向)
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
tools:context=".MainActivity">
<Button
android:id="@+id/button1"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="button1"/>
<Button
android:id="@+id/button2"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="button2"/>
<Button
android:id="@+id/button3"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="button3"/>
</LinearLayout>
注意:如果LinerLayout的排列方向是horizontal,那么内部控件绝对不能将宽度指定为match_parent,否则一个控件就会将水平方向占满,其他控件就没有可放置的位置了。同样的道理,如果LinerLayout的排列方向是vertical,内部的控件就不能将高度指定为match_parent。
再看android:layout_gravity属性,这个属性看起来android:gravity属性很像。但android:gravity用于指定文字在控件中的对齐方式,而android:layout_gravity指定控件在布局中的对齐方式。 android:layout_gravity的可选值和android:gravity差不多,但是要注意,当LinearLayout的排列方向是horizontal时,只有垂直方向上的对齐方式才会生效。因为水平方向的长度不固定,每添加一个控件,水平方向上的长度都会改变,因此无法指定水平方向上的对齐方式。同理,当LinearLayout的排列方式是vertiacl是,只有水平方向上的排列方式才会生效。
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="horizontal"
tools:context=".MainActivity">
<Button
android:id="@+id/button1"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="top"
android:text="button1"/>
<Button
android:id="@+id/button2"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center_vertical"
android:text="button2"/>
<Button
android:id="@+id/button3"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="bottom"
android:text="button3"/>
</LinearLayout>
LinearLayout的另一个重要属性 android:layout_weight(权重)。这个属性允许我们使用比例的方式来指定控件的大小,它在手机屏幕的适配性方面可以起到非常重要的作用。 比如 编写一个消息发送页面,需要一个文本编辑框和发送按钮
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="horizontal"
tools:context=".MainActivity">
<EditText
android:id="@+id/input_message"
android:layout_width="0dp"
android:layout_weight="1"
android:layout_height="wrap_content"
android:hint="Type Something"/>
<Button
android:id="@+id/send"
android:layout_width="0dp"
android:layout_weight="1"
android:layout_height="wrap_content"
android:text="Send"/>
</LinearLayout>
这里将EditText和Button的宽度都指定成了0dp,但是由于我们使用了android:layout_weight属性,所以此时的控件宽度就不再由android:layout_width决定了,这里的0dp是一种比较规范的写法。 (注意,这里之所以将控件宽度设为0dp,高度设为wrap_content,是因为LinearLayout的排列方向是horizontal,将会分屏幕宽度。如果排列方向是vertical,那么就要将高度设置为0dp,宽度设置为wrap_content就会分屏幕高度)
在EditText和Button里面将android:layout_weight属性值指定为1,表示EditText和Button在水平方向平分宽度。
为什么EditText和Button里面将android:layout_weight属性值都指定为1就会平分屏幕宽度呢?是因为系统把LinearLayout下将所有控件指定的layout_weight值相加,得到总值,然后按值占总值的比例来分。所有要想EditText占屏幕宽度的3/5,Button占屏幕宽度的2/5.只需要将其layout_weight值分别设置为3和2即可。
<EditText
android:id="@+id/input_message"
android:layout_width="0dp"
android:layout_weight="1"
android:layout_height="wrap_content"
android:hint="Type Something"/>
<Button
android:id="@+id/send"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Send"/>
这里仅指定EditText的android:layout_weight属性,将Button的宽度按照wrap_content来计算。这样,Button的宽度会先按wrap_content来计算,而EditText会因为只有它设置了android:layout_weight为1,所以会占满屏幕。按照这种方式,就能适配各种屏幕,而且看起来更加美观。(注意,由于只有EditText指定了android:layout_weight属性,故这个值除以它本身,只要是大于0的数就能保证它会是1,即填满剩下的区域)
RelativeLayout
RelativeLayout又被称为相对布局,可以可以通过相对定位的方式,让控件出现在布局的任何位置。
<Button
android:id="@+id/button1"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentLeft="true"
android:layout_alignParentTop="true"
android:text="button1"/>
<Button
android:id="@+id/button2"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentTop="true"
android:layout_alignParentRight="true"
android:text="button2"/>
<Button
android:id="@+id/button3"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_centerInParent="true"
android:text="button3"/>
<Button
android:id="@+id/button4"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentLeft="true"
android:layout_alignParentBottom="true"
android:text="button4"/>
<Button
android:id="@+id/button5"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentBottom="true"
android:layout_alignParentRight="true"
android:text="button5"/>
align是边缘的意思,所以这里 相当于直接相对父布局进行定位,如 button5 就是位于父布局的右下,和父布局的底部边缘和右边边缘对齐。同理button4位于左下角。
让控件相对于控件进行定位
<Button
android:id="@+id/button3"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_centerInParent="true"
android:text="button3"/>
<Button
android:id="@+id/button1"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_above="@id/button3"
android:layout_toLeftOf="@id/button3"
android:text="button1"/>
<Button
android:id="@+id/button2"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_above="@id/button3"
android:layout_toRightOf="@id/button3"
android:text="button2"/>
<Button
android:id="@+id/button4"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_below="@id/button3"
android:layout_toLeftOf="@id/button3"
android:text="button4"/>
<Button
android:id="@+id/button5"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_below="@id/button3"
android:layout_toRightOf="@id/button3"
android:text="button5"/>
这里先让button3位于父布局的中央,然后根据button3进行相对定位。这里button5 就是位于button3的下面和右边,即右下的位置。其余同理,注意:当一个控件去引用另一个控件的id时,该控件一定要在引用控件的后面,不然会出现找不到id的情况。
RelativeLayout中还有一组相对于控件定位的属性,android:layout_alignLeft,表示让控件的左边缘和另一个控件的左边缘对齐。和父布局的那个同一个意思,不过这个是相对于控件的对齐。(而之前由于子控件在父布局的内部,所以之前子布局的右边缘和父布局的右边缘对齐,导致直接相贴。)
<Button
android:id="@+id/button1"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignLeft="@id/button3"
android:layout_above="@id/button3"
android:text="button1"/>
这样按钮1 就会位于按钮3的上方,并且按钮1的左侧会与按钮3的左侧对齐。
FrameLayout
FrameLayout 又叫帧布局,这种布局没有丰富的定位方式,所有控件默认摆放在布局的左上角。
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="horizontal"
tools:context=".MainActivity">
<TextView
android:id="@+id/textView"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="This is TextView"/>
<Button
android:id="@+id/button"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Button"/>
</FrameLayout>
文字和按钮默认摆放布局的左上角。由于Button是在TextView之后添加的,因此按钮压在了文字上面。 除了这种默认的效果,我们还可以使用layout_gravity属性来指定控件在布局中的对齐方式,类似于LinearLayout.
创建自定义控件
引入布局
一般程序中需要很多Activity需要标题栏,如果每一个Activity都编写同一样的标题栏,你们会导致代码大量重复。所以可以通过引入布局的方式来解决这个问题。
在layout目录下新建一个title.xml布局
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:orientation="horizontal"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="@drawable/title_bg">
<Button
android:id="@+id/titleBack"
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/titleText"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:gravity="center"
android:layout_gravity="center"
android:text="Title Text"
android:textSize="24sp"
android:textColor="#fff"/>
<Button
android:id="@+id/titleEdit"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:layout_margin="5dp"
android:background="@drawable/edit_bg"
android:text="Edit"
android:textColor="#fff"
/>
</LinearLayout>
注意,这里LinearLayout的android:layout_height设置成了wrap_content,是为了限制标题栏的高度。
android:background用于为布局或控件指定一个背景,可以用颜色或图片来填充。 android:layout_margin属性指定控件在上下左右方向上的间距。如果使用android:layout_marginLeft或者android:layout_marginTop,则是单独指定某个方向上的间距。
标题栏布局完成后,使用的方法如下
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".MainActivity">
<include layout="@layout/title"/>
</LinearLayout>
只需要通过include 语句就能引入标题栏布局了。
但是要注意,将引入布局的Activity中系统自带的标题栏隐藏掉。
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
supportActionBar?.hide()
}
这里调用了getSupportActionBar()方法获取了ActionBar实例,然后调用hide()方法将标题栏隐藏。由于ActionBar可能为空,所以还使用了?.操作符。
创建自定义控件
引入布局的技巧解决了重复编写布局代码的问题,但是如果布局中有一些控件要求能够响应事件,我们需要还是需要为这些控件单独编写一次事件注册的代码。 比如标题栏中的返回按钮的事件,其实不管在哪个Activity中,这个返回按钮的功能都是相同的,即销毁当前的Activity。而如果每个Activity都需要重新注册一遍返回按钮的点击事件,会增加很多的重复代码,所以这种情况最好用自定义控件的方式来解决。
新建一个TitleLayout继承自LinearLayout,让它成为自定义的标题栏控件。
class TitleLayout(context: Context,attrs: AttributeSet):LinearLayout(context,attrs){
init {
LayoutInflater.from(context).inflate(R.layout.title,this)
}
在TitleLayout的主构造函数中声明了Context和AttributeSet两个参数,在布局时引入TitleLayout控件时就会调用这个构造函数。在init结构体中需要对标题栏布局进行动态加载,这里借助LayoutInflater来实现,通过LayoutInflater的from()方法可以构建出一个LayoutInflater对象,然后调用inflate()方法就可以动态加载一个布局文件。 inflate()方法接收两个参数,第一个参数是要加载的布局文件id,这里传入R.layout,title;第二个参数是给加载好的布局再添加一个父布局,这里指定为TitleLayout,于是传入this。
这里的inflate()方法把xml布局文件变成了view(加载过程),然后添加到了父布局中
**Kotlin中的init代码块就相当于Java中的普通代码块,在创建对象的时候代码块会先执行。**注意是每次创建都会执行一遍。
添加自定义控件和添加普通控件的方式一样,只不过在添加自定义控件的时候,需要指明控件的完整包名,包名在这里不可省略。
此时的效果和使用引入布局的效果一致,但我们还可以为自定义控件添加响应事件。
class TitleLayout(context: Context,attrs: AttributeSet):LinearLayout(context,attrs){
init {
LayoutInflater.from(context).inflate(R.layout.title,this)
titleBack.setOnClickListener {
val activity = context as Activity
activity.finish()
}
titleEdit.setOnClickListener {
Toast.makeText(context,"You clide Edit button" ,Toast.LENGTH_SHORT).show()
}
}
}
注意: TitleLayout中接收的context参数实际上是一个Activity的实例,所以在返回按钮的点击事件里,首先要将它转换成Activity类型,然后再调用finish()方法销毁当前的Activity。 Kotlin中的类型强制转换使用的关键字是as
这里Toast.makeText的第一个参数是context,也就是上下文。所以我们这里的第一个参数不能用this,如果用this,指代的就是TitleLayout。而如果用context,指代的是传入进来的Activity实例。Toast希望在Activity中展出
这样,我们在一个布局中引入TitleLayout时,返回按钮和编辑按钮的点击事件已经自动实现好了,就省了很多重复代码的工作。
ListView
由于手机屏幕空间比较有限,能够一次性在屏幕上显示的内容并不多,所以当程序中有大量数据需要展示的时候,就可以借助ListView来实现。ListView允许用户通过手指上下滑动的方式将屏幕外的数据滚动到屏幕内,同时屏幕上原有的数据会滚动出屏幕。
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".MainActivity">
<ListView
android:id="@+id/listView"
android:layout_width="match_parent"
android:layout_height="match_parent"/>
</LinearLayout>
在布局中引入一个ListView的控件不难,为ListView指定一个id,然后设置宽度和高度。
修改MainActivity代码,如下:
class MainActivity : AppCompatActivity() {
private val data = listOf("1","2","3","4","5","6","7","8","9","10","11","12",
"13","14","15","16")
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
val adapter = ArrayAdapter<String>(this,android.R.layout.
simple_expandable_list_item_1,data)
listView.adapter=adapter
}
}
ListView是用于展示大量数据的,那么我们就应该先将数据提供好。这些数据可以从网上下载,也可以从数据库中读取,应该视应用场景而定。 这里使用一个data集合来测试,初始化集合用内置的listOf()函数。
不过,集合中的数据无法直接传递给ListView,需要借助适配器来完成。 Andorid 中提供了很多的适配器的实现类,好用的主要是ArrayAdapter.它可以通过泛型来指定要适配的数据类型,然后在构造函数中把要适配的数据传入。 ArrayAdapter有多个构造函数的重载,应该根据实际情况选择适合的那一种。 由于提供的数据都是字符串,因此将ArrayAdapter的泛型指定为String,然后在ArrayAdapter的构造函数中依次传入Activity的实例,ListView子项布局的id,以及数据源。 ( ArrayAdapter<>(activity,resource ,data))
注意:这里使用了android.R.layout.simple_expandable_list_item_1 作为ListView子项布局的id,这是Android内置的布局文件,里面只有一个TextView,可用于简单地显示一段文本。这样适配器的对象就构建好了。
最后调用了ListView的setAdapter()方法,将构建好的适配器对象传递进去,这样ListView和数据之间的关联就构建好了。
定制ListView的界面
对ListView的界面进行定制,让它可以显示更加丰富的内容.
首先定义一个实体类,作为ListView适配器的适配类型
class Fruit (val name :String,val imageId :Int)
Fruit类中只有两个字段:name表示名字,imageId表示水果对应图片的资源id
为ListView的子项指定一个自定义的布局,在layout目录下新建fruit_item_xml
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:orientation="vertical"
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用于显示水果的名称,并且设置布局为垂直.
创建一个自定义的适配器,适配器去继承ArrayAdapter并将泛型指定为Fruit类 (前面定义的,用来作为适配器的适配类型的实体类)
新建类FruitAdapter(自定义适配器)
class FruitAdapter(activity: Activity,val resourceId: Int,data :List<Fruit>)
:ArrayAdapter<Fruit>(activity,resourceId,data){
override fun getView(position: Int, convertView: View?, parent: ViewGroup): View {
val view = LayoutInflater.from(context).inflate(resourceId,parent,false)
val fruitImage :ImageView = view.findViewById(R.id.fruitImage)
val fruitName :TextView = view.findViewById(R.id.fruitName)
val fruit = getItem(position)
if(fruit!=null)
{
fruitImage.setImageResource(fruit.imageId)
fruitName.text = fruit.name
}
return view
}
}
在FruitAdapter中定义了一个主构造函数,用于将Activity的实例,ListView子项布局id和数据源传进来。并且重写了getView()方法,这个方法在每个子项被滚动到屏幕内的时候会被调用。
在getView()方法中,首先使用LaoutInflater来为这个子项加载传入的布局. LayoutInflater的inflate()方法接收3个参数,前两个分别是 ListView的子项布局id和父布局。第三个参数指定为false,表示只让我们在父布局中声明的layout属性失效,但不会为这个View添加父布局。因为一旦View有了父布局之后,它就不能再添加到ListView中了。 这是ListView的标准写法,含义再理解。
然后调用了View的findViewByid()方法分别获取到ImageView和TextView实例,然后通过getItem()的方法得到了当前项的Fruit实例,并且分别调用它们的setImageResource()和setText()方法设置显示的文字和图片,最后将布局返回,这样自定义的适配器就完成了。
注意:kotlin-android-exetensions插件在ListView的适配器中也能正常工作。 (kotlin-android-exetensions插件会根据布局文件中定义的控件id自动生成一个相同名称的变量,从而可以直接在Activity中使用这个变量,不需要再调用findViewById()方法了 Toast的初次运用那有所涉及这个插件)
所以,上述代码中的两处findViewById()方法分别替换成view.fruitImage和view.fruitName。效果是一样的
注意:这里在适配器中使用kotlin-android-exetensions插件 和直接在Activity中调用 不同,这里view. 的原因是:要将传入的布局作为主体,所以直接用fruitImage 这个变量是不可以的。而在一般的Activity中,直接使用插件生成的变量名即对于布局文件中的控件id。比如想在MainActivity为控件id为button1的按钮设置监听器,在MainActivity代码中,直接通过 button1.setOnclickListener即可。
最后修改主Activity中的代码:
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
}
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传入,然后把创建好的对象添加到水果列表中去。(注意,这里实体类Fruit 构造函数的第二个参数 即图片的资源id是Int型) 这里为了将数据量填满屏幕,使用了repeat函数将所有水果数据添加了两遍。 repeat函数是Kotlin中另一个非常常用的标准函数,它允许传入一个数值n,然后就会将lambda表达式中内容执行n遍。
在onCreate()方法中创建了一个FruitAdapter对象,并将它作为适配器传递给ListView,这样定制的ListView界面任务就完成了。
提高ListView的运行效率
ListView控件难用,是因为有很多细节可以优化,其中运行效率就是很重要的一点。比如 在前面的FruitAdapter的getView()方法中,每次布局都将重新加载一遍,当listView快速滚动的时候,这会成为性能的瓶颈.
getView()方法中还有一个convertView参数,这个参数用于将之前加载好的布局进行缓存,以便之后进行重用,可以借助这个参数来进行性能优化。
重写FruitAdapter类中的getView()方法
override fun getView(position: Int, convertView: View?, parent: ViewGroup): View {
val view:View
if(convertView==null) {
view = LayoutInflater.from(context).inflate(resourceId,parent,false)
}
else {
view = convertView
}
val fruitImage :ImageView = view.fruitImage
val fruitName :TextView = view.fruitName
val fruit = getItem(position)
if(fruit!=null)
{
fruitImage.setImageResource(fruit.imageId)
fruitName.text = fruit.name
}
return view
}
在getView()方法中加了判断:如果convertView为null,则使用LayoutInflater去加载布局;如果不为null,则直接对convertView进行重用。这样就大大提高了ListView的运行效率,在快速滚动的时候可以表现出更好的性能。
不过仍然可以继续优化,因为虽然现在不会再重复去加载布局,但每次在getView()方法中仍然会调用View的findViewByid()方法来获取一次实例。 所以可以借助一个ViewHolder来对这部分性能进行优化。
修改FruitAdapter中的代码
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.fruitImage
val fruitName :TextView = view.fruitName
viewHolder = ViewHolder(fruitImage, fruitName)
view.tag = viewHolder
}
else {
view = convertView
viewHolder= view.tag as ViewHolder
}
val fruit = getItem(position)
if(fruit!=null)
{
viewHolder.fruitImage.setImageResource(fruit.imageId)
viewHolder.fruitName.text = fruit.name
}
return view
}
}
这里新增一个内部类 ViewHolder,用于对ImageView和TextView的控件进行缓存,Kotlin中使用inner class 关键字 来定义内部类。 当convertView为null的时候,创建一个ViewHolder对象,并将控件的实例存放在ViewHolder中,然后调用View的setTag()方法,将ViewHolder对象那存储在View中,当ConverView不为null的时候,则调用View的getTag()方法,把ViewHolder重新取出。这样所有控件的缓存都存放在了ViewHolder里,就没有必要每次都通过findViewById()方法来获取控件实例了。
这里由于控件的实例都存放在ViewHolder中,所以最后设置显示的图片和文字的时候,要通过ViewHolder的实例来调用。
ListView的点击事件
为ListView的子项添加点击事件
修改MainActivity中的代码
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 { parent, view, position, id ->
val fruit =fruitList[position]
Toast.makeText(this,fruit.name,Toast.LENGTH_SHORT).show()
}
}
使用setOnItemClickListener()方法为ListView注册了一个监听器,当用户点击了ListView中任何一个子项的时,就会回调到Lambda表达式中。 这里通过position参数判断用户点击的是哪一个子项,然后获取到响应的水果,并通过Toast将水果的名字显示出来。
上述代码中Lambda表达式在参数列表中声明了4个参数,但如何知道需要声明哪几个参数呢?按住ctrl 键,左键点击即可进入方法·查看源码。 由于是Lambda表达式,也就代表这是一个Java单抽象方法接口,否则无法使用函数式API的写法。
另外,虽然待实现方法中声明了4个参数,但是实际上只用到了positon这一个参数。针对这种情况,Kotlin允许我们将没用到的参数使用下划线来代替。
listView.setOnItemClickListener { _, _, position, _ ->
val fruit =fruitList[position]
Toast.makeText(this,fruit.name,Toast.LENGTH_SHORT).show()
}
注意,即使将没有用到的参数使用下划线来代替,它们之间的位置也不能改变。这里postion参数仍然得在第三个参数的位置。
|