第一行代码Android个人笔记(二)——UI开发基础

本节内容对应《第一行代码 第三版》:第四章

文章目录

    • 一、AlertDialog
    • PS:隐藏标题栏
    • 二、ListView
      • 1. 简单显示一串文本,需要两步:
      • 2. 定制ListView的界面
      • 3.优化
      • 4.实现点击事件
    • 三、RecycleView的用法
      • 1、在gradle中引入依赖库
      • 2、在布局文件中添加相应的recycleView组件
      • 3、自建继承自RecyclerView.Adapter的布局适配器
      • 4、在MainActivity中载入布局
      • - 疑难点:ViewHolder是什么?
      • 5、其他布局模式
      • 6、添加点击事件
    • 四、编写一个对话程序
      • 1、9-Patch图片
      • 2、编写相应的主界面布局和两个子界面布局
      • 3、建立消息的实体类,方便管理每个消息对象
      • 4、创建自己的MsgAdapter
      • 5、载入布局
      • 6、再谈RecycleView复用机制:onCreateView和onBindView调用关系
    • 五、Kotlin课堂——延迟初始化

一、AlertDialog

详解参见:Android之AlertDialog的基本使用

AlertDialog.Builder(this).apply {
     
                setTitle("This is a dialog")
                setMessage("something important")
                setCancelable(false)//点击对话框以外的区域是否让对话框消失,默认为true
                setPositiveButton("OK"){
     
                        dialog, which -> {
     
                }
                }
                setNegativeButton("Cancel"){
     
                        dialog, which ->
                }
            }.show()

PS:隐藏标题栏

supportActionBar?.hide()

二、ListView

1. 简单显示一串文本,需要两步:

  • 在要显示列表的布局中引入ListView
<ListView
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:id="@+id/listView"
    />
  • 在逻辑代码中设置适配器
val data = listOf<String>("apple","banana","a","d","dd","ad","dfasf","df","apple","banana","a","d","dd","ad","dfasf","df","apple","banana","a","d","dd","ad","dfasf","df")
        //参数:Activity实例,ListView子项布局id,数据源
        val adapter = ArrayAdapter<String>(this,android.R.layout.simple_list_item_1,data)//这里使用了一个默认的子项布局R.layout.simple_list_item_1
        listView.adapter = adapter//设置适配器

2. 定制ListView的界面

  • 在上一个简单实现的基础上,我们不再使用默认的子项布局R.layout.simple_list_item_1,而是自己新建一个layout文件作为子项布局,从而做到自定义,这里使用fruit_item

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

    <ImageView
        android:layout_width="40dp"
        android:layout_height="40dp"
        android:id="@+id/fruitImage"
        android:layout_gravity="center_vertical"
        android:layout_marginLeft="10dp"/>

    <TextView
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:id="@+id/fruitName"
        android:layout_gravity="center_vertical"
        android:layout_marginLeft="10dp"/>

LinearLayout>
  • 然后,我们也不再使用默认的适配器ArrayAdapter,而是自己新建一个类FruitAdapter,继承ArrayAdapter,并在getView函数中完成数据和子项布局的绑定
//接收ArrayAdapter的三个参数:Activity实例,ListView子项布局id,数据源
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)//暂时理解为标准写法

        //在ListView的适配器里无法使用import kotlinx.android.synthetic.main.activity_main.*插件,所以只能先获取控件
        val fruitImage:ImageView = view.findViewById(R.id.fruitImage)
        val fruitName:TextView = view.findViewById(R.id.fruitName)

        //获取当前fruit实例
        val fruit = getItem(position)
        if (fruit != null){
     
            fruitImage.setImageResource(fruit.imageId)
            fruitName.text = fruit.name
        }
        return view
    }
}

另外,为了方便,新建了一个类Fruit,用于存放每份的数据资源

class Fruit (val name:String, val imageId:Int) {
     }
  • 最后,修改在Activity中的数据绑定,使用新的FruitAdapterfruit_item
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))
        }
    }

3.优化

由于在每个子项被滚动到屏幕内的时候都会调用getView这个方法,这会导致
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)
反复加载,使得效率降低。
所以用getView()自带的一个参数convertView: View?缓存界面,并使用内部类viewHolder暂时存储控件的实例,并把viewHolder存储在view.tag

//接收ArrayAdapter的三个参数:Activity实例,ListView子项布局id,数据源
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)//暂时理解为标准写法

            //在ListView的适配器里无法使用import kotlinx.android.synthetic.main.activity_main.*插件,所以只能先获取控件
            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
        }

        //获取当前fruit实例
        val fruit = getItem(position)
        if (fruit != null){
     
            viewHolder.fruitImage.setImageResource(fruit.imageId)
            viewHolder.fruitName.text = fruit.name
        }
        return view
    }
}

4.实现点击事件

listView.setOnItemClickListener {
      parent, view, position, id ->
            val fruit = fruitList[position]
            Toast.makeText(this,fruit.name,Toast.LENGTH_SHORT).show()
        }
  • 也可以添加长按事件
    第一行代码Android个人笔记(二)——UI开发基础_第1张图片

三、RecycleView的用法

1、在gradle中引入依赖库

    implementation 'androidx.recyclerview:recyclerview:1.1.0'

2、在布局文件中添加相应的recycleView组件

使用约束布局

3、自建继承自RecyclerView.Adapter的布局适配器

/*RecycleView适配器的标准写法*/
//RecyclerView.Adapter为了优化性能,自带一个FruitAdapter.ViewHolder的泛型,用于缓存界面数据
//参数fruitList: List是数据源
class FruitAdapter(val fruitList: List<Fruit>) : RecyclerView.Adapter<FruitAdapter.ViewHolder>() {
     
    //这里使用内部类的方式创建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)
    }

    /*以下三个函数是自动重写*/
    //这里创建ViewHolder的实例,并加载布局,最后将ViewHolder的实例返回
    override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
     
        val view = LayoutInflater.from(parent.context).inflate(R.layout.fruit_item,parent,false)
        return ViewHolder(view)
    }

    //用于告诉recycleView一共有多少子项,返回数据源的长度
    override fun getItemCount(): Int {
     
        return fruitList.size
    }

    //在每个子项被滚动到屏幕的时候执行,用于对recycleView的子项布局中的组件进行赋值
    override fun onBindViewHolder(holder: ViewHolder, position: Int) {
     
        val fruit = fruitList[position]
        holder.fruitImage.setImageResource(fruit.imageId)
        holder.fruitName.text = fruit.name
    }
}

4、在MainActivity中载入布局

class MainActivity : AppCompatActivity() {
     

    private val fruitList = ArrayList<Fruit>()

    override fun onCreate(savedInstanceState: Bundle?) {
     
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)

        initFruits()
        //设置布局方式,LinearLayoutManager是线性布局
        val layoutManager = LinearLayoutManager(this)
        recycleView.layoutManager = layoutManager
        //绑定适配器
        val adapter = FruitAdapter(fruitList)
        recycleView.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))
        }
    }
}

- 疑难点:ViewHolder是什么?

viewHolder是recyclerView缓存管理的对象,每一个item项目都有一个对应的viewHolder用来保存界面组件。
在item首次被滑动进界面时,每一个viewHolder将会在函数onCreateViewHolder中进行初始化,并留下相应的缓存(这个缓存保存在ArrayList mCachedViews里),并同时进行viewHolder和数据的绑定,即调用onCreateViewHolder和onBindViewHolder各一次。
当item被划出界面时,程序会自动回收那些已经看不见的item,但是他们的viewHolder缓存仍然被保存着,当这个item再次被划入时,就不需要对该item的界面组件进行初始化,即不需要调用onCreateViewHolder,只需要调用onBindViewHolder绑定数据即可。

  • 也可以参见大神的博客:RecycleView 复用机制onCreateView和onBindView调用关系

5、其他布局模式

  • 横向布局:
layoutManager.orientation = LinearLayoutManager.HORIZONTAL
  • 九宫格布局(垂直方向上的,4代表个数)
recy_view.setLayoutManager(new GridLayoutManager(this,4));
  • 九宫格布局(水平方向上的)
recy_view.setLayoutManager(new GridLayoutManager(this, 4, LinearLayoutManager.HORIZONTAL, false));
  • 瀑布布局(3代表列数)
val layoutManager = StaggeredGridLayoutManager(3,StaggeredGridLayoutManager.VERTICAL)

6、添加点击事件

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{
     
            //在这里设置item的点击事件
        }
        viewHolder.fruitImage.setOnClickListener {
     
            //在这里设置fruitImage的点击事件
        }
        viewHolder.fruitName.setOnClickListener {
     
            //在这里设置fruitName的点击事件
        }

        return viewHolder
    }

四、编写一个对话程序

1、9-Patch图片

与普通图片不同,使用9-Patch图片作为屏幕或按钮的背景时,当屏幕的尺寸或按钮的大小改变时,图片可自动缩放,达到不失真的效果。

  • 先将普通图片创建为9-Patch图片第一行代码Android个人笔记(二)——UI开发基础_第2张图片
  • 然后在编辑视图中使用黑框标记:
    • 左边和上边黑色边是可以拉伸部分,即可以随着设置的大小在手机上显示不同的效果。
    • 下侧边和右侧边的黑色边是可显示区域。

第一行代码Android个人笔记(二)——UI开发基础_第3张图片

  • 最后别忘记删除原来的图片

2、编写相应的主界面布局和两个子界面布局

activity_main.xml


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

    <androidx.recyclerview.widget.RecyclerView
        android:layout_width="match_parent"
        android:layout_height="0dp"
        android:id="@+id/recyclerView"
        android:layout_weight="1"/>

    <LinearLayout
        android:layout_width="match_parent"
        android:layout_height="wrap_content">

        <EditText
            android:layout_width="0dp"
            android:layout_height="wrap_content"
            android:layout_weight="1"
            android:hint="Type something here"
            android:maxLines="2"
            android:id="@+id/inputText"/>

        <Button
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:text="Send"
            android:id="@+id/send"/>

    LinearLayout>
LinearLayout>

msg_left_item.xml



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

    <LinearLayout
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_gravity="left"
        android:background="@drawable/message_left_original">

        <TextView
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:id="@+id/leftMsg"
            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="match_parent">

    <LinearLayout
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_gravity="right"
        android:background="@drawable/message_right_original">

        <TextView
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_gravity="center"
            android:layout_margin="10dp"
            android:textColor="#000"
            android:id="@+id/rightMsg"/>

    LinearLayout>

FrameLayout>

3、建立消息的实体类,方便管理每个消息对象

//消息的实体类
class Msg(val content:String, val type:Int) {
     

    //先前讲过的伪静态方法
    companion object{
     
        //定义常量,只有在companion object或顶层方法中才能使用const
        const val TYPE_RECEIVED = 0
        const val TYPE_SEND = 1
    }
}

4、创建自己的MsgAdapter

class MsgAdapter(val msgList: List<Msg>): RecyclerView.Adapter<RecyclerView.ViewHolder>() {
     

    //创建了两个不同的ViewHolder
    inner class LeftViewHolder(view: View): RecyclerView.ViewHolder(view){
     
        val leftMsg = view.findViewById<TextView>(R.id.leftMsg)
    }

    inner class RightViewHolder(view: View): RecyclerView.ViewHolder(view){
     
        val rightMsg = view.findViewById<TextView>(R.id.rightMsg)
    }

    //不重写的话,会默认返回0,现在重写,让它返回我们自己定义的类型(整形变量),以便于在onCreateViewHolder获取对于的viewType,绑定对应的ViewHolder
    /*源码:
            public int getItemViewType(int position) {
            return 0;
        }
    * */
    override fun getItemViewType(position: Int): Int {
     
        val msg = msgList[position]
        return msg.type
    }

    override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder {
     
        Log.d("ceshi", "调用一次onCreateViewHolder")
        //返回对应的viewHolder
        if (viewType ==  Msg.TYPE_RECEIVED){
     
            val view = LayoutInflater.from(parent.context).inflate(R.layout.msg_left_item,parent,false)
            return LeftViewHolder(view)
        }else{
     
            val view = LayoutInflater.from(parent.context).inflate(R.layout.msg_right_item,parent,false)
            return RightViewHolder(view)
        }
    }

    override fun getItemCount(): Int {
     
        return msgList.size
    }

    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
        }
        Log.d("ceshi", "调用一次onBindViewHolder")
    }
}

5、载入布局

class MainActivity : AppCompatActivity() {
     

    private val msgList:ArrayList<Msg> = ArrayList()

    override fun onCreate(savedInstanceState: Bundle?) {
     
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)

        initMsgList()

        val adapter: MsgAdapter = MsgAdapter(msgList)
        recyclerView.adapter = adapter
        val layoutManager = LinearLayoutManager(this)
        recyclerView.layoutManager = layoutManager

        send.setOnClickListener {
     
            val content = inputText.text.toString()
            if (content.isNotEmpty()){
     
                val msg = Msg(inputText.text.toString(),Msg.TYPE_SEND)
                msgList.add(msg)

                //通知列表有新的数据插入,从而刷新新的数据
                //同类型函数:adapter.notifyDataSetChanged()会刷新所有的数据
                adapter.notifyItemInserted(msgList.size - 1)

                //将显示的数据定位的最后一行
                recyclerView.scrollToPosition(msgList.size - 1)
                inputText.setText("")
            }
        }
    }

    fun initMsgList(){
     
        repeat(8){
     
            var msg = Msg("Hello guy", Msg.TYPE_RECEIVED)
            msgList.add(msg)
            msg = Msg("Hello. Who is that?", Msg.TYPE_SEND)
            msgList.add(msg)
            msg = Msg("This is Tom", Msg.TYPE_RECEIVED)
            msgList.add(msg)
        }
    }
}

6、再谈RecycleView复用机制:onCreateView和onBindView调用关系

  • 可以看到,所有消息第一次载入时,日志是这样的:先进行onCreateViewHolder,再进行onBindViewHolder
    第一行代码Android个人笔记(二)——UI开发基础_第4张图片
  • 界面向下滚动到底部:由于数据都是重新刷新的,所以还是 先进行onCreateViewHolder,再进行onBindViewHolder
    第一行代码Android个人笔记(二)——UI开发基础_第5张图片
  • 再次向上滚动:可以看到,后面全部变成了onBindViewHolder,不需要再次创建ViewHolder了
    第一行代码Android个人笔记(二)——UI开发基础_第6张图片

五、Kotlin课堂——延迟初始化

如果我们明确知道一个变量一定会在其他位置进行初始化,而不像定义如下的可空变量时:

private var ad: MsgAdapter? = null

我们可以这样来进行延迟初始化:

private lateinit var ad: MsgAdapter

你可能感兴趣的:(Android开发技术,android)