低级安卓工程师之路(四)

常见控件的使用方法

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)//获取当前项的Fruit实例
        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)//获取当前项的Fruit实例

        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参数仍然得在第三个参数的位置。

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

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值