4 UI开发
01 该如何编写程序界面
Android应用程序的界面主要是通过编写XML的方式来实现的。写XML的好处是,我们不仅能够了解界面背后的实现原理,而且编写出来的界面还可以具备很好的屏幕适配性。
Google又推出了一个全新的界面布局:ConstraintLayout。和以往传统的布局不同,ConstraintLayout不是非常适合通过编写XML的方式来开发界面,而是更加适合在可视化编辑器中使用拖放控件的方式来进行操作,并Android Studio中也提供了非常完备的可视化编辑器。
02 常用控件的使用方法
1 TextView
用法
<TextView
android:id="@+id/textView"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:gravity="center"
android:textColor="#00ff00"
android:textSize="24sp"
android:text="This is TextView"/>
2 Button
用法
button.setOnClickListener{
}
class MainActivity : AppCompatActivity() ,View.OnClickListener{
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main4)
}
override fun onClick(v: View?) {
when(v?.id){
R.id.button -> println("button is clicked")
}
}
}
3 EditText
用法
<EditText
android:id="@+id/editText"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:hint="Type something here"
android:maxLines="2"
/>
当输入的内容超过两行时,文本就会向上滚动,EditText则不会再继续拉伸
4 ImageView
主流分辨率:xxhdpi. 新建一个drawable-xxhdpi目录
设置数据源
imageView.setImageResource(R.drawable.img_2)
5 ProgressBar
用法
//进度条消失
when (v?.id) {
R.id.button -> {
if (progressBar.visibility == View.VISIBLE) {
progressBar.visibility = View.GONE
} else {
progressBar.visibility = View.VISIBLE
}
}
}
-
visible表示控件是可见的,这个值是默认值,不指定android:visibility时,控件都是可见的。
-
invisible表示控件不可见,但是它仍然占据着原来的位置和大小,可以理解成控件变成透明状态了。
-
gone则表示控件不仅不可见,而且不再占用任何屏幕空间。
水平进度条
<ProgressBar
android:id="@+id/progressBar"
android:layout_width="match_parent"
android:layout_height="wrap_content"
style="?android:attr/progressBarStyleHorizontal"
android:max="100"
/>
动态设置进度
when (v?.id) {
R.id.button -> progressBar.progress = progressBar.progress + 10
}
6 AlertDialog
用法
R.id.button -> {
AlertDialog.Builder(this).apply {
setTitle("This is Dialog")
setMessage("Something important.")
setCancelable(false)
setPositiveButton("OK") { dialog, which ->
Toast.makeText(context,"OK",Toast.LENGTH_SHORT).show()
}
setNegativeButton("Cancel") { dialog, which ->
}
show()
}
}
//java
AlertDialog.Builder builder = new AlertDialog.Builder(this);
builder.setIcon(R.mipmap.ic_launcher)
.setTitle("这是一个对话框")
.setMessage("这是消息")
.setView(dialogView)
.setPositiveButton("确定", new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which) {
Log.e(TAG, "onClick: 确定" );
}
})
.setNegativeButton("取消", new DialogInterface.OnClickListener() {
@Overrid面实现,图4.15很好地展示了它们之间的关系。e
public void onClick(DialogInterface dialog, int which) {
Log.e(TAG, "onClick: 取消" );
}
})
.setNeutralButton("中间", new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which) {
Log.e(TAG, "onClick: 中间" );
}
})
.create()
.show();
03 详解3种布局
布局是一种可用于放置很多控件的容器,它可以按照一定的规律调整内部控件的位置,从而编写出精美的界面。
1 LinearLayout
LinearLayout又称作线性布局,是一种非常常用的布局。
当然,如果不指定android:orientation属性的值,默认的排列方向就是horizontal。
android:gravity用于指定文字在控件中的对齐方式,而android:layout_gravity用于指定控件在布局中的对齐方式。android:layout_gravity的可选值和android:gravity差不多,但是需要注意,当LinearLayout的排列方向是horizontal时,只有垂直方向上的对齐方式才会生效。当LinearLayout的排列方向是vertical时,只有
水平方向上的对齐方式才会生效。
指定layout_weight的效果
2 RelativeLayout
RelativeLayout又称作相对布局,可以通过相对定位的方式让控件出现在布局的任何位置。
相对父类容器
android:layout_alignParentLeft
android:layout_alignParentTop
android:layout_alignParentRight
android:layout_alignParentBottom
android:layout_centerInParent
相对空间布局
-
android:layout_above属性可以让一个控件位于另一个控件的上方,需要为这个属性指定相对控件id的引用,这里我们填入了@id/button3,表示让该控件位于Button 3的上方。其他的属性也是相似的
-
android:layout_below表示让一个控件位于另一个控件的下方
-
android:layout_toLeftOf表示让一个控件位于另一个控件的左侧
-
android:layout_toRightOf表示让一个控件位于另一个控件的右侧。
另外
android:layout_alignLeft表示让一个控件的左边缘和另一个控件的左边缘对齐
android:layout_alignRight表示让一个控件的右边缘和另一个控件的右边缘对齐。其他类似。
3 FrameLayout
FrameLayout又称作帧布局,它相比于前面两种布局就简单太多了,因此它的应用场景少了很多。这种布局没有丰富的定位方式,所有的控件都会默认摆放在布局的左上角。
04 自定义控件
可以看到,我们所用的所有控件都是直接或间接继承自View的,所用的所有布局都是直接或间接继承自ViewGroup的。View是Android中最基本的一种UI组件,它可以在屏幕上绘制一块矩形区域,并能响应这块区域的各种事件,因此,我们使用的各种控件其实就是在View的基础上又添加了各自特有的功能。而ViewGroup则是一种特殊的View,它可以包含很多子View和子ViewGroup,是一个用于放置控件和布局的容器。
1 引入布局
使用include引入布局
<include layout="@layout/title" />
2 自定义控件
title.xml
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="wrap_content"
>
<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="#ff00ff"
android:text="Back"
android:textColor="#fff" />
<TextView
android:id="@+id/titleText"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:layout_weight="1"
android:gravity="center"
android:text="Title Text"
android:textColor="#fff"
android:textSize="24sp" />
<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="#ff00ff"
android:text="Edit"
android:textColor="#fff" />
</LinearLayout>
新建titleLayout继承LinearLayout
class TitleLayout(context: Context, attrs: AttributeSet) : LinearLayout(context, attrs) {
init {
LayoutInflater.from(context).inflate(R.layout.activity_main4, this)
titleBack.setOnClickListener {
val activity = context as Activity
activity.finish()
}
titleEdit.setOnClickListener {
Toast.makeText(context, "You clicked Edit button", Toast.LENGTH_SHORT).show()
}
}
}
这里我们在TitleLayout的主构造函数中声明了Context和AttributeSet这两个参数,在布局中引入TitleLayout控件时就会调用这个构造函数。然后在init结构体中需要对标题栏布局进行动态加载,这就要借助LayoutInflater来实现了。通过LayoutInflater的from()方法可以构建出一个LayoutInflater对象,然后调用inflate()方法就可以动态加载一个布局文件。inflate()方法接收两个参数:第一个参数是要加载的布局文件的id,这里我们传入R.layout.title;第二个参数是给加载好的布局再添加一个父布局,这里我们想要指定为TitleLayout,于是直接传入this。
之后我们可以使用新定义的控件了。
05 最常用、难用控件ListView
1 简单用法
private val data = listOf("Apple", "Banana", "Orange", "Watermelon",
"Pear", "Grape", "Pineapple", "Strawberry", "Cherry", "Mango",
"Apple", "Banana", "Orange", "Watermelon", "Pear", "Grape",
"Pineapple", "Strawberry", "Cherry", "Mango")
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main4)
val adapter = ArrayAdapter<String>(this,android.R.layout.simple_list_item_1,data)
listView.adapter = adapter
}
2 定制页面 fruit_item.xml
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:orientation="vertical"
android:layout_width="match_parent"
android:layout_height="match_parent">
<ImageView
android:id="@+id/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>
水果类
class Fruit(val name:String,val imageId:Int) {
}
Adapter
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
//viewhHolder避免重复find布局中的view
val viewHolder:ViewHolder
//convertView进行缓存,提高运行效率,避免重复加载布局
if (convertView == null) {
view = LayoutInflater.from(context).inflate(resourceId, parent, false)
val fruitImage:ImageView = view.findViewById(R.id.fruitImage)
val fruitName:TextView = view.findViewById(R.id.fruitName)
viewHolder = ViewHolder(fruitImage, fruitName)
view.tag = viewHolder
} else {
view = convertView
viewHolder = view.tag as ViewHolder
}
val fruit = getItem(position) //获取当前项的fruit实例
if (fruit!=null){
viewHolder.fruitImage.setImageResource(fruit.imageId)
viewHolder.fruitName.text = fruit.name;
}
return view
}
}
在activity中使用
class MainActivity4 : AppCompatActivity() {
private val data = listOf("Apple", "Banana", "Orange", "Watermelon",
"Pear", "Grape", "Pineapple", "Strawberry", "Cherry", "Mango",
"Apple", "Banana", "Orange", "Watermelon", "Pear", "Grape",
"Pineapple", "Strawberry", "Cherry", "Mango")
private val fruitList = ArrayList<Fruit>()
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main4)
initFruits()
val adapter = FruitAdapter(this,R.layout.fruit_item,fruitList)
//1
listView.adapter = adapter
//2.点击事件
listView.setOnItemClickListener { _, _, position, _ ->
val fruit = fruitList[position]
Toast.makeText(this, fruit.name, Toast.LENGTH_SHORT).show()
}
}
private fun initFruits() {
repeat(2) {
fruitList.add(Fruit("Apple", R.drawable.ic_launcher_background))
fruitList.add(Fruit("Banana",R.drawable.ic_launcher_background))
fruitList.add(Fruit("Orange", R.drawable.ic_launcher_background))
fruitList.add(Fruit("Watermelon", R.drawable.ic_launcher_background))
fruitList.add(Fruit("Pear", R.drawable.ic_launcher_background))
fruitList.add(Fruit("Grape", R.drawable.ic_launcher_background))
fruitList.add(Fruit("Pineapple", R.drawable.ic_launcher_background))
fruitList.add(Fruit("Strawberry", R.drawable.ic_launcher_background))
fruitList.add(Fruit("Cherry", R.drawable.ic_launcher_background))
fruitList.add(Fruit("Mango", R.drawable.ic_launcher_background))
}
}
}
06 RecyclerView
1 基本使用
添加依赖
implementation 'androidx.recyclerview:recyclerview:1.0.0'
当你不能确定最新的版本号是多少的时候,可以就像上述代码一样填入1.0.0,当有更新的库版本时,Android Studio会主动提醒你,并告诉你最新的版本号是多少.
水果类fruit
adapter
class FruitAdapter(val fruitList:List<Fruit>): RecyclerView.Adapter<FruitAdapter.ViewHolder>() {
inner class ViewHolder(view:View):RecyclerView.ViewHolder(view){
val fruitImage:ImageView = view.findViewById(R.id.fruitImage)
val fruitName:TextView = view.findViewById(R.id.fruitName)
}
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
val view = LayoutInflater.from(parent.context).inflate(R.layout.fruit_item,parent,false)
return ViewHolder(view)
}
override fun onBindViewHolder(holder: ViewHolder, position: Int) {
val fruit = fruitList[position]
holder.fruitImage.setImageResource(fruit.imageId);
holder.fruitName.text = fruit.name
}
override fun getItemCount() = fruitList.size
}
这是RecyclerView适配器标准的写法,虽然看上去好像多了好几个方法,但其实它比ListView的适配器要更容易理解。这里我们首先定义了一个内部类ViewHolder,它要继承自RecyclerView.ViewHolder。然后ViewHolder的主构造函数中要传入一个View参数,这个参数通常就是RecyclerView子项的最外层布局,那么我们就可以通过findViewById()方法来获取布局中ImageView和TextView的实例了。
继续往下看,由于FruitAdapter是继承自RecyclerView.Adapter的,那么就必须重写onCreateViewHolder()、onBindViewHolder()和getItemCount()这3个方法。
-
onCreateViewHolder()方法是用于创建ViewHolder实例的,我们在这个方法中将fruit_item布局加载进来,然后创建一个ViewHolder实例,并把加载出来的布局传入构造函数当中,最后将ViewHolder的实例返回。
-
onBindViewHolder()方法用于对RecyclerView子项的数据进行赋值,会在每个子项被滚动到屏幕内的时候执行,这里我们通过position参数得到当前项的Fruit实例,然后再将数据设置到ViewHolder的ImageView和TextView当中即可。
-
getItemCount()方法就非常简单了,它用于告诉RecyclerView一共有多少子项,直接返回数据源的长度就可以了。
在activity中使用
class MainActivity4 : AppCompatActivity() {
private val data = listOf("Apple", "Banana", "Orange", "Watermelon",
"Pear", "Grape", "Pineapple", "Strawberry", "Cherry", "Mango",
"Apple", "Banana", "Orange", "Watermelon", "Pear", "Grape",
"Pineapple", "Strawberry", "Cherry", "Mango")
private val fruitList = ArrayList<Fruit>()
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main4)
initFruits()
val layoutManager = LinearLayoutManager(this)
layoutManager.orientation = LinearLayoutManager.HORIZONTAL
recyclerView.layoutManager = layoutManager
val adapter = FruitAdapter(fruitList)
recyclerView.adapter = adapter
}
private fun initFruits() {
repeat(2) {
fruitList.add(Fruit("Apple", R.drawable.ic_launcher_background))
fruitList.add(Fruit("Banana",R.drawable.ic_launcher_background))
fruitList.add(Fruit("Orange", R.drawable.ic_launcher_background))
fruitList.add(Fruit("Watermelon", R.drawable.ic_launcher_background))
fruitList.add(Fruit("Pear", R.drawable.ic_launcher_background))
fruitList.add(Fruit("Grape", R.drawable.ic_launcher_background))
fruitList.add(Fruit("Pineapple", R.drawable.ic_launcher_background))
fruitList.add(Fruit("Strawberry", R.drawable.ic_launcher_background))
fruitList.add(Fruit("Cherry", R.drawable.ic_launcher_background))
fruitList.add(Fruit("Mango", R.drawable.ic_launcher_background))
}
}
}
LayoutManager用于指定RecyclerView的布局方式,这里使用的LinearLayoutManager是线性布局的意思,可以实现和ListView类似的效果。接下来我们创建了FruitAdapter的实例,并将水果数据传入FruitAdapter的构造函数中,最后调用RecyclerView的setAdapter()方法来完成适配器设置,这样RecyclerView和数据之间的关联就建立完成了。
2 实现横向滚动和瀑布流
横向滚动,设置recyclerView的layoutmanager即可
val layoutManager = LinearLayoutManager(this)
layoutManager.orientation = LinearLayoutManager.HORIZONTAL
recyclerView.layoutManager = layoutManager
瀑布流,设置recyclerView的layoutmanager即可。为了让瀑布流更直观,text的长度使用getRandomLengthString变成随机长度。
val layoutManager = StaggeredGridLayoutManager(3,StaggeredGridLayoutManager.VERTICAL) //3是一行有几个
recyclerView.layoutManager = layoutManager
private fun initFruits() {
repeat(2) {
fruitList.add(Fruit(getRandomLengthString("Apple"), R.drawable.ic_launcher_background))
fruitList.add(Fruit(getRandomLengthString("Banana"),R.drawable.ic_launcher_background))
fruitList.add(Fruit(getRandomLengthString("Orange"), R.drawable.ic_launcher_background))
fruitList.add(Fruit(getRandomLengthString("Watermelon"), R.drawable.ic_launcher_background))
fruitList.add(Fruit(getRandomLengthString("Pear"), R.drawable.ic_launcher_background))
fruitList.add(Fruit(getRandomLengthString("Grape"), R.drawable.ic_launcher_background))
fruitList.add(Fruit(getRandomLengthString("Pineapple"), R.drawable.ic_launcher_background))
fruitList.add(Fruit(getRandomLengthString("Strawberry"), R.drawable.ic_launcher_background))
fruitList.add(Fruit(getRandomLengthString("Cherry"), R.drawable.ic_launcher_background))
fruitList.add(Fruit(getRandomLengthString("Mango"), 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()
}
3 点击事件
设置adapter的onCreateViewHolder即可设置点击事件。
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
val view = LayoutInflater.from(parent.context).inflate(R.layout.fruit_item,parent,false)
val viewHolder = ViewHolder(view)
viewHolder.itemView.setOnClickListener {
val position = viewHolder.adapterPosition
val fruit = fruitList[position]
Toast.makeText(parent.context, "you clicked view ${fruit.name}",
Toast.LENGTH_SHORT).show()
}
viewHolder.fruitImage.setOnClickListener{
val position = viewHolder.adapterPosition
val fruit = fruitList[position]
Toast.makeText(parent.context, "you clicked image ${fruit.name}",
Toast.LENGTH_SHORT).show()
}
return viewHolder
}