基础UI设计

基础UI界面设计

2021.2.16

Gary哥哥的哥哥的哥哥

如何编写程序界面

通过编写XML的方式来实现

  • 不过Google推出的ConstraintLayout不是非常适合通过编写XML的方式来开发界面,而是适合在可视化编辑器下拖放控件来实现界面设计
    • 虽然Google现在更加倾向于让大家使用ConstraintLayout来开发程序界面
    • 但作为初学者,我们先对ConstraintLayout进行非常详细的讲解先。

下面是我用constraintLayout来编写的一个小小Demo,详见Basic_UI_Demo的项目

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-AgbEHi5c-1613750288646)(E:\kotlin-study\Studying-Kotlin\UI\UI_demo\constraint.png)]

常用控件

TextView

match_parent表示当前控件的大小和父布局的大小一样
wrap_content表示让当前控件的大小刚好能够包含住里面的全部内容
<?xml version="1.0" encoding="utf-8"?>
<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">
<!--    match_parent表示当前控件的大小和父布局的大小一样; wrap_content表示让当前控件的大小刚好能够包含住里面的全部内容-->

    <TextView
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="Hello World!"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintLeft_toLeftOf="parent"
        app:layout_constraintRight_toRightOf="parent"
        app:layout_constraintTop_toTopOf="parent" />
</LinearLayout>
android:gravity="center_vertical|center_horizontal"

|: 指定多个属性值

距离用dp做单位,字体用sp做单位

  • 其他属性我就不一一介绍了,我们在实践时候自己去探索吧

Button

 <Button
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:id="@+id/button1"
        android:text="Button1"
        />

默认Button上的字母都是大写,可以通过属性修改

  • android:textAllCaps="false"

下面通过kotlin来响应事件

package com.workaholiclab.control

import androidx.appcompat.app.AppCompatActivity
import android.os.Bundle
import android.util.Log
import kotlinx.android.synthetic.main.activity_main.*

class MainActivity : AppCompatActivity() {
    
    
    override fun onCreate(savedInstanceState: Bundle?) {
    
    
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)
        //Lambda表达式实现
        button1.setOnClickListener {
    
    
            //逻辑代码
            Log.d("respond","button1 is clicked")
        }
    }
}

除了上面这种Lambda表达式来实现之外,还可以函数API的方式来注册监听器

package com.workaholiclab.control

import androidx.appcompat.app.AppCompatActivity
import android.os.Bundle
import android.util.Log
import android.view.View
import kotlinx.android.synthetic.main.activity_main.*

//函数API来注册实现
class MainActivity : AppCompatActivity(),View.OnClickListener {
    
    
    override fun onCreate(savedInstanceState: Bundle?) {
    
    
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)
        button2.setOnClickListener(this)
        
    }

    override fun onClick(v: View?) {
    
    
        when(v?.id){
    
    
            R.id.button2->{
    
    
                Log.d("respond2","button2 is clicked")
            }
        }
    }
}
  • 上面这种写法我们让MainActivity实现了View.OnClickListener接口,并且重写了onClick方法,然后在调用button的setOnClickListener方法将Activity实例传递出去。

  • 每当我们点击按钮的时候就会执行Onclick的方法

EditText

    <EditText
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:id="@+id/editText1"
        />

hint属性可以提示信息

 <EditText
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:id="@+id/editText1"
        android:hint="Type something here"
        />

为了防止输入东西过多,导致EditText非常难看,我们用android:maxLines的属性

  • 下面代码表示EditText可以显示两行,以前的行不再显示,但还在的,往上滚了
<EditText
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:id="@+id/editText1"
        android:hint="Type something here"
        android:maxLines="2"
        />
class MainActivity : AppCompatActivity() {
    
    
    override fun onCreate(savedInstanceState: Bundle?) {
    
    
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)
        //Lambda表达式实现
        button1.setOnClickListener {
    
    
            //逻辑代码
            val input=editText1.text.toString()
            Toast.makeText(this,input,Toast.LENGTH_SHORT).show()
        }
    }
}

ImageView

src指定目录的图片

  <ImageView
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:id="@+id/imageView1"
        android:src="@drawable/abc_vector_test"
        />
//手动更改
button2.setOnClickListener {
    
     
            imageView1.setImageResource(R.drawable.ic_launcher_foreground)
        }

ProgressBar

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

 <ProgressBar
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:id="@+id/progressBar"
        />

那么,我们当数据加载完之后如何让他消失掉呢

  • 我们利用控件的可视化来做
 button1.setOnClickListener {
    
    
            val input=editText1.text.toString()
            Toast.makeText(this,input,Toast.LENGTH_SHORT).show()
            if(progressBar.visibility==View.VISIBLE){
    
    
                progressBar.visibility=View.GONE
            }
            else{
    
    
                progressBar.visibility=View.VISIBLE
            }
        }

我们还可以给ProgressBar指定样式

默认为转圈圈

下面改成水平的:

  <ProgressBar
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:id="@+id/progressBar"
        style="@style/Widget.AppCompat.ProgressBar.Horizontal"
        android:max="100"
        />
//增加进度   
button1.setOnClickListener {
    
    
            val input=editText1.text.toString()
            Toast.makeText(this,input,Toast.LENGTH_SHORT).show()
            if(progressBar.visibility==View.VISIBLE){
    
    
                progressBar.visibility=View.GONE
            }
            else{
    
    
                progressBar.visibility=View.VISIBLE
                progressBar.progress=progressBar.progress+10
            }
        }

AlertDialog

在当前用户见面弹出一个对话窗

  • 这个控件一般用于非常重要的内容或者是一些警告信息
    • 下面通过kotlin代码来做一个Dialog
    • 最后记得show出来
 button2.setOnClickListener {
    
    
            imageView1.setImageResource(R.drawable.ic_launcher_foreground)
            AlertDialog.Builder(this).apply {
    
    
                setTitle("This is a Dialog")
                setMessage("something Important")
                setCancelable(false)
                setPositiveButton("OK"){
    
    
                    dialog, which ->
                }
                setNegativeButton("Cancel"){
    
    
                    dialog, which ->
                }
                show()
            }
        }

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-7fUtP0Cq-1613750288647)(E:\kotlin-study\Studying-Kotlin\UI\UI_demo\dialog.png)]

空间部分我们先讲解那么多,其他控件及其属性可以通过实践来自行摸索

三种基本布局

LinearLayout

线性布局是一种非常常见的布局,我们上面写的代码示例都是线性布局的

  • 我们可以摄影水平和垂直的方向
<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"
    android:orientation="vertical">

设置权重

  • 0dp代表不再有width的值来决定宽度
    • 效果就是左右各一个
<?xml version="1.0" encoding="utf-8"?>
<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/ed1"
        android:layout_width="0dp"
        android:layout_height="wrap_content"
        android:layout_weight="1"
        android:hint="Type Someting"
        />
    <Button
        android:id="@+id/send"
        android:layout_width="0dp"
        android:layout_height="wrap_content"
        android:layout_weight="1"
        android:text="Send"/>
</LinearLayout>

更好的自适应效果:

<?xml version="1.0" encoding="utf-8"?>
<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/ed1"
        android:layout_width="0dp"
        android:layout_height="wrap_content"
        android:layout_weight="1"
        android:hint="Type Someting"
        />
    <Button
        android:id="@+id/send"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="Send"/>
</LinearLayout>

RelativeLayout

相对布局也是比较常用的一种布局,与LinearLayout不同的是,它相对显得较为随意

<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent" android:layout_height="match_parent">
<Button
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:id="@+id/button1"
    android:layout_alignParentLeft="true"
    android:layout_alignParentTop="true"
    />
<Button
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:id="@+id/button2"
    android:layout_alignParentRight="true"
    android:layout_alignParentTop="true"
    />
<Button
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:id="@+id/button3"
    android:layout_alignParentLeft="true"
    android:layout_alignParentBottom="true"
    />
<Button
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:id="@+id/button4"
    android:layout_alignParentRight="true"
    android:layout_alignParentBottom="true"
    />
<Button
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:id="@+id/button5"
    android:layout_centerInParent="true"
    />
</RelativeLayout>

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-qOwlHGpX-1613750288648)(E:\kotlin-study\Studying-Kotlin\UI\UI_demo\relative.png)]

还可以相对于其他控件进行布局

<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent" android:layout_height="match_parent">
<Button
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:id="@+id/button1"
    android:layout_below="@+id/button5"
    android:layout_toRightOf="@+id/button5"
    />
<Button
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:id="@+id/button2"
    android:layout_below="@+id/button5"
    android:layout_toLeftOf="@+id/button5"
    />
<Button
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:id="@+id/button3"
    android:layout_above="@+id/button5"
    android:layout_toRightOf="@+id/button5"
    />
<Button
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:id="@+id/button4"
    android:layout_above="@+id/button5"
    android:layout_toLeftOf="@+id/button5"
    />
<Button
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:id="@+id/button5"
    android:layout_centerInParent="true"
    />
</RelativeLayout>

相对于中间那个Button来进行布局

FrameLayout

帧布局,这种布局是非常非常简单的同时它的引用场景确实也比较少

  • 不过在后面的Fragment的介绍当中还会有用处的
    • 下面就是一左一右:
<?xml version="1.0" encoding="utf-8"?>
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent" android:layout_height="match_parent">
<TextView
    android:id="@+id/tv1"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:layout_gravity="left"
    android:text="This is a TextView"
    />
    
    <Button
        android:id="@+id/bt1"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_gravity="right"
        android:text="Button"/>
</FrameLayout>

创建自定义的控件

引入布局

在系统控件不够用的时候,还可以创建自定义的控件

其实那些控件继承都是:TextView 或者 ImageView 或者 ViewGroup**(准确点说 EditText,Button继承TextView,其他都是继承ViewGroup)**

  • 而他们的祖先都是View
    • 这里做一个类似于一起IOS风格的布局我就不做展示了,可以见书本P169的内容
    • 一个xml布局,多个activity共用
    • Button可以设置backGround属性来加入自己喜欢的形状

当我们写了一个frame.xml的布局之后,想在其他layout当中调用:

<include layout="@layout/frame"/>
//隐藏标题栏
class MainActivity : AppCompatActivity() {
    
    
    override fun onCreate(savedInstanceState: Bundle?) {
    
    
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)
        supportActionBar?.hide()
    }
}

自定义控件

我们创建TitleLayout继承LinearLayout

  • 这里我们以自定义一个标题栏控件
    • 与添加普通控件时候略有不同的是,添加自定义控件的时候,完整的类名和包名是不可省略的
class TitleLayout(context:Context,attrs:AttributeSet):LinearLayout(context,attrs) {
    
    
    init {
    
    
        LayoutInflater.from(context).inflate(R.layout.test,this)
    }
}

在布局中引入TitileLayout会调用这个构造函数,在init结构体中会进行动态加载,借助LayoutInflater来实现

  • from()方法构建出一个LayoutInflater对象
  • inflate()动态加载一个布局文件
    • 第一个参数是要加载的布局文件id
    • 第二个参数是加载好的布局再添加一个布局

现在自定义控件已经创建好了,接下来我们需要在布局文件中添加这个自定义控件

修改main_activity.xml:

<?xml version="1.0" encoding="utf-8"?>
<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">

   <com.workaholiclab.manifcontrol.TitleLayout
       android:layout_width="match_parent"
       android:layout_height="wrap_content"/>

</LinearLayout>

test.xml如下:

<?xml version="1.0" encoding="utf-8"?>
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
    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="left"
        android:text="Back"/>

    <Button
        android:id="@+id/button2"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_gravity="right"
        android:text="Edit"/>
</FrameLayout>
class TitleLayout(context:Context,attrs:AttributeSet):LinearLayout(context,attrs) {
    
    
    init {
    
    
        LayoutInflater.from(context).inflate(R.layout.test,this)
        button1.setOnClickListener {
    
     val activity=context as Activity
        activity.finish()
        }
        button2.setOnClickListener {
    
     Toast.makeText(context,"you clicked Edit button",Toast.LENGTH_SHORT).show() }
    }
}
button1.setOnClickListener { val activity=context as Activity
activity.finish()

这里TitleLayout中接受的context参数实际上是一个Activity的实例,类型转化为Activity,然后调用finish()方法

  • as 为强制类型转换

最难的控件:ListView

数据滚动出屏幕

  • ex: QQ聊天记录, 翻看微博的信息…
<?xml version="1.0" encoding="utf-8"?>
<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:layout_width="match_parent"
        android:layout_height="match_parent"
        android:id="@+id/lv1"/>
</LinearLayout>
class MainActivity : AppCompatActivity() {
    
    
    private val data= listOf<String>("Apple","Banana","Pear","Watermelon","Grape","Pineapple","Strawberry","Cherry","Mango","Orange")
    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)
        lv1.adapter=adapter
    }
}

下面这段代码就是关键

  • 把Adapter构建好:val adapter=ArrayAdapter<String>(this,android.R.layout.simple_list_item_1,data)
  • 将适配好的Adapter对象传递给listView的
val adapter=ArrayAdapter<String>(this,android.R.layout.simple_list_item_1,data)
lv1.adapter=adapter

自定义ListView

我们给单调的文字稍微加一点图片吧!!!

  • 定义一个实体类 Fruit 和fruit_item.xml
class Fruit(val name:String,val imageId:Int) {
    
    
    
}
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:orientation="vertical" android:layout_width="match_parent"
    android:layout_height="60dp">
    
    <ImageView
        android:layout_width="40dp"
        android:layout_height="40dp"
        android:layout_gravity="center_vertical"
        android:layout_marginLeft="10dp"
        android:id="@+id/fruitImage"
        />
    
    <TextView
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_gravity="center_vertical"
        android:layout_marginLeft="10dp"
        android:id="@+id/fruitName"/>

</LinearLayout>
  • 新建FruitAdapter

    这次Adapt是这个Fruit的对象了

    • 自定义适配器!!!
    • 很重要哦
    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实例
            fruit?.let {
          
           fruitImage.setImageResource(fruit.imageId)
            fruitName.text=fruit.name}
            return view
        }
    }
    

说明:

  • LayoutInflater的inflate()接受的三个参数,前面两个我们已经知道是怎么回事了:
    • 第一个参数是要加载的布局文件id
    • 第二个参数是加载好的布局再添加一个布局
    • 第三个参数false就是表示只让我们在父布局中声明的layout失效,但不会为这个View添加父布局
  • 这就是ListView标准的写法
  • 加入实例
    • repeat函数,后面跟一个Lambda表达式:
repeat(2){
    
    
    for (fn in data)
    {
    
    
        fruitList.add(Fruit(fn,R.drawable.ic_launcher_background))
    }
package com.workaholiclab.listviewtest

import androidx.appcompat.app.AppCompatActivity
import android.os.Bundle
import android.widget.ArrayAdapter
import kotlinx.android.synthetic.main.activity_main.*

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)
        lv1.adapter=adapter
//        val adapter=ArrayAdapter<String>(this,android.R.layout.simple_list_item_1,data)
//        lv1.adapter=adapter
    }

    private fun initFruits() {
    
    
        val data= listOf<String>("Apple","Banana","Pear","Watermelon","Grape","Pineapple","Strawberry","Cherry","Mango","Orange")
        repeat(2){
    
    
            for (fn in data)
            {
    
    
                fruitList.add(Fruit(fn,R.drawable.ic_launcher_background))
            }
        }
    }
}

上面的关于listView的全部代码,可能有的部分需要自行做一些小小修改

  • 我这里没有这些水果的图片我随便放一张图片上去就算了

提升ListView的效率

ListView之所以难用,因为它很多的细节都是可以优化的,运行效率就是其中的一个优化点

  • 效率低的原因是getView()方法中每次都要将布局重新加载一遍,当其快速滚动的时候,其性能就会成为瓶颈
  • 我们使用convertView参数来进行缓存!!!

重写getView()方法

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:View = convertView ?: 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实例
        fruit?.let {
    
     fruitImage.setImageResource(fruit.imageId)
        fruitName.text=fruit.name}
        return view
    }
}

继续优化

现在每次在getView方法中仍然还是会调用View的findViewById的方法来获取一次控件的实例

  • 现在我们借助一个ViewHolder来对这一部分进行优化
  • 使用内部内inner class实现所有控件的缓存都放在ViewHolder中,没有必要每次findViewById()
class FruitAdapter(activity: Activity,val resourceId:Int, data:List<Fruit>): ArrayAdapter<Fruit>(activity,resourceId,data) {
    
    
    inner class ViewHolder(val fruitImageView: ImageView,val fruitName: TextView)
        override fun getView(position: Int, convertView: View?, parent: ViewGroup): View {
    
    
            val viewHolder:ViewHolder
            val view:View
            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实例
            fruit?.let {
    
    
                viewHolder.fruitImageView.setImageResource(fruit.imageId)
                viewHolder.fruitName.text=fruit.name
            }
            return view
        }

}

经过上述两步的优化后,性能已经非常不错了

ListView的点击事件

为ListView注册一个监听器即可

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)
        lv1.adapter=adapter
        lv1.setOnItemClickListener {
    
     parent, view, position, id ->
            val fruit=fruitList[position]
            Toast.makeText(this,fruit.name,Toast.LENGTH_SHORT).show()
        }
//        val adapter=ArrayAdapter<String>(this,android.R.layout.simple_list_item_1,data)
//        lv1.adapter=adapter
    }
    private fun initFruits() {
    
    
        val data= listOf<String>("Apple","Banana","Pear","Watermelon","Grape","Pineapple","Strawberry","Cherry","Mango","Orange")
        repeat(2){
    
    
            for (fn in data)
            {
    
    
                fruitList.add(Fruit(fn,R.drawable.ic_launcher_background))
            }
        }
    }
}

没有用到的参数Kotlin还允许用下划线_代替

lv1.setOnItemClickListener {
    
     _, _, position, _ ->
    val fruit=fruitList[position]
    Toast.makeText(this,fruit.name,Toast.LENGTH_SHORT).show()
}

RecyclerView

更加强大的滚动组件

  • 他是一个增强版本的ListView,未来的更多程序会从ListView转向RecyclerView

基本用法

implementation 'androidx.recyclerview:recyclerview:1.0.0'

具体到最新版本可以自行查找

下面是RecyclerView适配器的标准写法

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 getItemCount(): Int {
    
    
        return fruitList.size
    }

    override fun onBindViewHolder(holder: ViewHolder, position: Int) {
    
    
        val fruit=fruitList[position]
        holder.fruitImage.setImageResource(fruit.imageId)
        holder.fruitName.text=fruit.name
    }
}

上面这个RecyclerView适配器的写法看起来好复杂,里面的方法很多,但其实比ListView更加简洁

  • 首先我们定义一个内部内ViewHolder,它继承自RecyclerView.ViewHolder
  • 然后ViewHoder的主构造函数中要传入一个View的参数
    • 这个参数是RecyclerView子项的最外层布局
    • 可以通过findViewById的方法获取布局中的IamgeView和TextView实例
  • FruitAdapter中有一个主构造函数,FruitAdapter(val fruitList:List<Fruit>)他用于把展示的数据传进来,方便我们后续操作
  • 重写父类RecyclerView.Adapter<FruitAdapter.ViewHolder>的三个方法:
    • onCreateViewHolder()创建一个ViewHolder实例,并把加载出来的布局传到构造函数里面,最后将ViewHolder的实例返回
    • onBindViewHolder()方法用于对RecyclerView子项的数据进行赋值,会在每个子项被滚动到屏幕内的时候执行,这里我们通过position参数得到当前项的Fruit实例,然后再设置到ViewHolder中
    • getItemCount方法就是返回多少个子项就好了

适配器准备好之后,我们使用RecyclerView如下:

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)
        rv1.layoutManager=layoutManager
        val adapter=FruitAdapter(fruitList)
        rv1.adapter=adapter
    }
    private fun initFruits() {
    
    
        val data= listOf<String>("Apple","Banana","Pear","Watermelon","Grape","Pineapple","Strawberry","Cherry","Mango","Orange")
        repeat(2){
    
    
            for (fn in data)
            {
    
    
                fruitList.add(Fruit(fn,R.drawable.ic_launcher_background))
            }
        }
    }
}
  • 先创建LinearLayoutManager对象,并把它设置到RecyclerView
  • LayoutManager指定RecyclerView的布局方式
  • 最后调用RecyclerView的setAdapter方法来完成调用即可

实现横向布局与瀑布布局

要想实现横向滚动的话就修改一下fruit_item.xml的代码

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="80dp"
    android:layout_height="wrap_content"
    android:orientation="vertical">

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

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

</LinearLayout>
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)
        rv1.layoutManager=layoutManager
        layoutManager.orientation=LinearLayoutManager.HORIZONTAL
        val adapter=FruitAdapter(fruitList)
        rv1.adapter=adapter
    }
    private fun initFruits() {
    
    
        val data= listOf<String>("Apple","Banana","Pear","Watermelon","Grape","Pineapple","Strawberry","Cherry","Mango","Orange")
        repeat(2){
    
    
            for (fn in data)
            {
    
    
                fruitList.add(Fruit(fn,R.drawable.ic_launcher_background))
            }
        }
    }
}

加多了一行layoutManager.orientation=LinearLayoutManager.HORIZONTAL即可,默认是纵向的!

LayoutManager就是RecyclerView的最大优势所在

  • 制定了一套可扩展的布局排列接口,子类只要按照接口的规范来实现,就能定制出不同的排列布局了

下面我们来看看瀑布布局

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:layout_margin="5dp"
    android:orientation="vertical">

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

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

</LinearLayout>
class MainActivity : AppCompatActivity() {
    
    
    private val fruitList=ArrayList<Fruit>()
    override fun onCreate(savedInstanceState: Bundle?) {
    
    
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)
        initFruits()
        val layoutManager=StaggeredGridLayoutManager(3,StaggeredGridLayoutManager.VERTICAL)
        rv1.layoutManager=layoutManager
        val adapter=FruitAdapter(fruitList)
        rv1.adapter=adapter
    }
    private fun initFruits() {
    
    
        val data= listOf<String>("Apple","Banana","Pear","Watermelon","Grape","Pineapple","Strawberry","Cherry","Mango","Orange")
        repeat(2){
    
    
            for (fn in data)
            {
    
    
                fruitList.add(Fruit(getRandomLengthString(fn),R.drawable.ic_launcher_background))
            }
        }
    }

    private fun getRandomLengthString(str: String): String {
    
    
        val n=(1..20).random()
        val builder=StringBuilder()
        repeat(n){
    
    
            builder.append(str)
        }
        return builder.toString()
    }
}

核心代码如下:

        val layoutManager=StaggeredGridLayoutManager(3,StaggeredGridLayoutManager.VERTICAL)
        rv1.layoutManager=layoutManager
        val adapter=FruitAdapter(fruitList)
        rv1.adapter=adapter

效果图:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-RkNVP8Xa-1613750288648)(E:\kotlin-study\Studying-Kotlin\UI\瀑布布局.png)]

点击事件

这一点和ListView相比,比较特殊,它没有方法实现,需要在自己给子项具体的View去注册点击事件

  • 这里相对于ListView来说确实要复杂一些

修改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 view ${
      
      fruit.name}",Toast.LENGTH_SHORT).show()
        }
        return viewHolder
    }

注意最够返回的,这里比较麻烦的是图片和文字都要响应这个点击事件

编写界面的最佳实践

这一部分主要内容自己看书本P193页开始吧

  • 这里我们也来讲解一下
  • 编写一个精美的聊天界面

制作9-Patch图片

这是一种经过特殊处理的png图片,能够制定那个区域被拉伸,哪些区域不可以被拉伸

在对应的资源图片右击学Create 9-Patch file即可

  • 黑色部分表示可以拉伸
  • 按住Shift+鼠标可以擦除
  • 最后记得把原来那张图片删除就好了
    • 因为android是不允许有两张的,即使后缀名不同也不可以

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-jXMbKIex-1613750288650)(E:\kotlin-study\Studying-Kotlin\UI\9patch.png)]

修改activity_main.xml代码:

android:background="#d8e0e8"
<?xml version="1.0" encoding="utf-8"?>
<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来制作

具体导入这里就不再做过多的阐述了

  • 新建Msg实体类

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

    只有在单例类,顶层方法,companion object中才可以使用const关键字

    接下来开始写RecyclerView的子布局了

    左右各一个

    <?xml version="1.0" encoding="utf-8"?>
    <FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
        android:orientation="vertical"
        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>
    
<?xml version="1.0" encoding="utf-8"?>
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:orientation="vertical"
    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>

Adapter的代码也是差不多:

package com.example.uibestpractice

import android.view.LayoutInflater
import android.view.ViewGroup
import androidx.recyclerview.widget.RecyclerView


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

    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: MsgViewHolder, position: Int) {
    
    
        val msg = msgList[position]
        when (holder) {
    
    
            is LeftViewHolder -> holder.leftMsg.text = msg.content
            is RightViewHolder -> holder.rightMsg.text = msg.content
         }
    }

    override fun getItemCount() = msgList.size

}

上面的代码我们根据不同的ViewType创建不同的界面

我们使用封闭类来实现:

sealed class MsgViewHolder(view: View) : RecyclerView.ViewHolder(view)

class LeftViewHolder(view: View) : MsgViewHolder(view) {
    
    
    val leftMsg: TextView = view.findViewById(R.id.leftMsg)
}

class RightViewHolder(view: View) : MsgViewHolder(view) {
    
    
    val rightMsg: TextView = view.findViewById(R.id.rightMsg)
}
  • 发送点击事件就很简单了
class MainActivity : AppCompatActivity(), View.OnClickListener {
    
    

    private val msgList = ArrayList<Msg>()

    private lateinit var adapter: MsgAdapter

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

    override fun onClick(v: View?) {
    
    
        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)
    }
}

这里你喜欢用另外一种风格的来写也是可以的

效果如下:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Vm3C4y3A-1613750288652)(E:\kotlin-study\Studying-Kotlin\UI\bestpractice.png)]

  • 再补充一个封闭内
sealed class Result

class Success(val msg: String) : Result()

class Failure(val error: Exception) : Result()


fun getResultMsg(result: Result) = when (result) {
    
    
    is Success -> result.msg
    is Failure -> "Error is ${
      
      result.error.message}"
}

UI基础设计部分正式完工

猜你喜欢

转载自blog.csdn.net/Garyboyboy/article/details/113874257
今日推荐