控件基础知识 宽度高度的取值
match_parent
: 由父布局来决定当前控件的大小
wrap_content
: 由控件内容决定当前控件的大小
固定值: 单位一般用 dp(一种屏幕密度无关的尺寸单位, 可以保证在不同分辨率的手机上显示效果尽可能地一致)
可见性的取值
visible
: 可见
invisible
: 不可见, 但是仍然占据着原来的位置和大小
gone
: 不可见, 且不再占用任何屏幕空间
基础控件
按钮中字母大写: :textAllCaps: "true"
事件
button1.setOnClickListener { Toast.makeText(this , "You click Button 1" , Toast.LENGTH_SHORT).show() }
事件的统一处理接口 将 Button 的点击事件统一交给 Activity 处理
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 class MainActivity : AppCompatActivity (), View.OnClickListener { override fun onCreate (savedInstanceState: Bundle ?) { super .onCreate(savedInstanceState) setContentView(R.layout.activity_main) val button_1 = findViewById<Button>(R.id.button_1) val button_2 = findViewById<Button>(R.id.button_2) button_1.setOnClickListener(this ) button_2.setOnClickListener(this ) } override fun onClick (view: View ?) { Log.d("TEMP" , (view == null ).toString()) when (view?.id) { R.id.button_1 -> { Toast.makeText(this , "Button 1" , Toast.LENGTH_SHORT).show() } R.id.button_2 -> { Toast.makeText(this , "Button 2" , Toast.LENGTH_SHORT).show() } else -> { Toast.makeText(this , "Button None" , Toast.LENGTH_SHORT).show() } } } }
TextView
文字对齐方式: :gravity
, 取值有(top, bottom, start, end, center
); 使用 |
同时指定多个值
文字颜色: :textColor="#00ff00"
文字大小: :textSize="24sp"
使用 sp 作为单位,这样当用户在系统中修改了文字显示尺寸时,应用程序中的文字大小也会跟着变化
Toast 在窗口中进行提示 Toast.makeText(Context, Content, Time)
:
Context:Toast 要求的上下文,由于 Activity 本身就是一个 Context 对象,因此通常为 this
Content: 显示的文本内容
Time: 显示的时长,有两个内置常量可以选择:Toast.LENGTH_SHORT
和 Toast.LENGTH_LONG
Toast.makeText(this , "You click Button 1" , Toast.LENGTH_SHORT).show()
新建 menu 资源: res/menu
+ res/menu/Menu_Resource_File_Name
在 Menu_Resource_File_Name
进行资源的添加: `
1 2 3 4 5 6 7 8 9 <?xml version="1.0" encoding="utf-8" ?> <menu xmlns:android ="http://schemas.android.com/apk/res/android" > <item android:id ="@+id/add_item" android:title ="Add" /> <item android:id ="@+id/remove_item" android:title ="Remove" /> </menu >
重写 Activity 中的 onCreateOptionsMenu()
方法
传入的 menu 指向的是当前 Activity 的菜单对象实例
menuInflater
实际上调用了父类中的 getMenuInflater()
方法获取到 MenuInflater
对象
然后调用 inflate()
方法给当前 Activity 创建菜单: 第一个参数为菜单资源,第二个参数为菜单上下文
返回 true 表示菜单创建后显示出来
1 2 3 4 override fun onCreateOptionsMenu (menu: Menu ?) : Boolean { menuInflater.inflate(R.menu.main, menu) return true }
响应事件
菜单项被点击: onOptionsItemSelected()
1 2 3 4 5 6 7 8 override fun onOptionsItemSelected (item: MenuItem ) : Boolean { when (item.itemId) { R.id.add_item -> Toast.makeText(this , "You clicked Add" , Toast.LENGTH_SHORT).show() R.id.remove_item -> Toast.makeText(this , "You clicked Remove" , Toast.LENGTH_SHORT).show() } return super .onOptionsItemSelected(item) }
图标和位置 在布局文件中通过 icon 属性指定菜单选项的图标, 通过 showAsAction 属性指定菜单选项的展示位置
always: 表示永远显示在 Toolbar 中, 空间不够则不显示
ifRoom: 表示优先显示在 Toolbar 中, 空间不够显示在菜单中
never: 表示只显示在菜单中
1 2 3 4 5 <item android:id ="@+id/backup" android:icon ="@drawable/ic_backup" android:title ="Backup" app:showAsAction ="always" />
自定义弹出的 Menu 菜单
准备 Menu 的资源文件
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 <?xml version="1.0" encoding="utf-8" ?> <menu xmlns:android ="http://schemas.android.com/apk/res/android" > <item android:id ="@+id/menu_info_by_day" android:icon ="@drawable/ic_meteor_64" android:title ="星迹" /> <item android:id ="@+id/menu_export_db" android:icon ="@drawable/ic_db_export_64" android:title ="导出数据" /> <item android:id ="@+id/menu_import_db" android:icon ="@drawable/ic_db_import_64" android:title ="导入数据" /> </menu >
初始化: 初始化时指定其弹出的位置(通过一个 view 对象指定)如果空间足够,它会显示在锚定 View 下方,否则显示在其上方
1 2 3 4 5 val popupMenu by lazy { val menu = PopupMenu(this , toolbarFragment.viewShowMenu) menu.menuInflater.inflate(R.menu.main_config, menu.menu) menu }
配置点击事件: 返回 True 表示点击事件被消耗
1 2 3 4 5 6 7 8 9 10 11 popupMenu.setOnMenuItemClickListener { when (it.itemId) { R.id.menu_info_by_day -> { TodoItemInfoByDayActivity.onActionStart(this ) true } else -> { true } } }
配置点击显示 Menu: 使用 MenuPopupHelper 进一步封装, 是为了在每一个 Menu 项中显示 Icon setForceShowIcon(true)
1 2 3 4 val menu = MenuPopupHelper(this , (popupMenu.getMenu() as MenuBuilder), toolbarFragment.viewShowMenu) menu.setForceShowIcon(true ) menu.show()
EditText
提示内容: :hint="Type something here"
最大行数: :maxLines="1"
超过最大行数会滚动显示(组件不会拉伸)
内容过滤模板: :inputType
常用的有 text, textPassword, number
方法:
获取内容: edit_username.text.toString()
设置内容: edit_username.setText(String)
设置光标位置: edit_username.setSelection(Int)
请求 Focus: requestFocus()
全选: selectAll()
ImageView
ProgressBar 进度条样式
默认旋转加载进度条
水平进度条: style="?android:attr/progressBarStyleHorizontal"
设定进度最大值: :max="100"
设定初始进度值: :progress="50"
获取进度值: int progress = progressBarHorizontal.getProgress();
设置进度值: progressBarHorizontal.setProgress(progress);
AlertDialog 在当前界面弹出一个对话框, 置于所有界面元素之上(屏蔽其他控件的交互能力)
二次确认 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 R.id.button_change_image -> { val environment = this AlertDialog.Builder(this ).apply { setTitle("Warning" ) setMessage("Do you really want to change the image source?" ) setCancelable(true ) setPositiveButton("YES" ) { dialog, which -> Toast.makeText(environment, "Change Image" , Toast.LENGTH_SHORT).show() val image_demo_1 = findViewById<ImageView>(R.id.image_demo_1) image_demo_1.setImageResource(R.drawable.image_demo_2) } setNegativeButton("Cancel" ) { dialog, which -> dialog.dismiss()} show() } }
列表选择 1 2 3 4 5 6 7 8 9 10 val todoCategoryItems = viewModel.todoCategoryList.map { it.name }.toTypedArray()val alertBuilder: AlertDialog.Builder = AlertDialog.Builder(this ).apply { setTitle("时间规划集" ) .setItems(todoCategoryItems, DialogInterface.OnClickListener { dialog, which -> insertTodoitmeCategory.setText(todoCategoryItems.get (which)) dialog.dismiss() }) create() show() }
ProgressDialog【OLD】 功能:弹出的对话框中显示一个进度条,缓解用户等待的焦躁。
构建方法:与AlertDialog类似
1 2 3 4 5 6 7 ProgressDialog progressDialog = new ProgressDialog (MainActivity.this );progressDialog.setTitle("Progress Dialog" ); progressDialog.setMessage("Loading..." ); progressDialog.setCancelable(true ); progressDialog.show();
布局 LinearLayout
布局方向: :orientation
属性排列方向有 vertical, horizontal
对齐方式: :layout_gravity
和文字在空间中的对齐方式类似
水平排列时, 水平长度不固定, 因此 center
会失效; 同理, …
空间比例排布: :layout_weight="1"
在一行或一列中按照比例分配大小, 自适应拉伸; 需要将 :layout_width
置为 0
布局技巧: 先 Layout 后 组件
RelativeLayout 通过相对定位的方式让控件出现在布局的任何位置
根据父组件布局:
:layout_alignParentLeft="true"
:layout_alignParentRight="true"
:layout_centerInParent="true"
:layout_alignParentTop="true"
:layout_alignParenBottom="true"
根据某一组件的位置布局:
:layout_toRightOf="@+id/button_center"
:layout_toLefttOf="@+id/button_center"
:layout_above="@+id/button_center"
:layout_below="@+id/button_center"
相对于组件对齐:
:layout_alignLeft
: 两组件左边缘对齐
:layout_alignRight
: 两组件右边缘对齐
:layout_alignTop
: 两组件上边缘对齐
:layout_alignBottom
: 两组件下边缘对齐
FrameLayout 帧布局, 所有的控件都会默认摆放在布局的左上角
自定义控件 所有控件都是直接或间接继承自 View:
View 是 Android 中最基本的一种 UI 组件,它可以在屏幕上绘制一块矩形区域,并能响应这块区域的各种事件
Application:
公共组件的复用 <include layout="@layout/title" />
: 标题栏, 尾部栏
需要将系统自带的标题栏等进行隐藏: supportActionBar?.hide()
但是如果复用的组件中有交互性事件, 如何复用这些事件的响应操作呢, 引入了自定义控件
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 class TitleLayout (context: Context, attrs: AttributeSet) : LinearLayout(context, attrs) { init { LayoutInflater.from(context).inflate(R.layout.title, this ) val titleBack = findViewById<Button>(R.id.titleBack) val titleText = findViewById<TextView>(R.id.titleText) val titleEdit = findViewById<Button>(R.id.titleEdit) titleBack.setOnClickListener { val activity = context as Activity activity.finish() } titleEdit.setOnClickListener { Toast.makeText(context, "Editing..." , Toast.LENGTH_SHORT).show() } } }
LayoutInflater
对象用于实现动态加载, 通过 LayoutInflater
的 from()
方法可以构建出一个 LayoutInflater
对象,然后调用 inflate()
方法就可以动态加载一个布局文件
第一个参数是要加载的布局文件的 id
第二个参数是给加载好的布局再添加一个父布局,这里我们想要指定为 TitleLayout,于是直接传入 this
在布局文件中添加这个自定义控件, 即可将相关的事件处理也一并复用引入:
<com.example.a05_uicustomviews.TitleLayout android:layout_width ="match_parent" android:layout_height ="wrap_content" />
ListView ListView 允许用户通过手指上下滑动的方式将屏幕外的数据滚动到屏幕内,同时屏幕上原有的数据会滚动出屏幕
Quick Start
准备好数据
1 2 3 4 private val data = listOf("Apple" , "Banana" , "Orange" , "Watermelon" , "Pear" , "Grape" , "Pineapple" , "Strawberry" , "Cherry" , "Mango" , "Apple" , "Banana" , "Orange" , "Watermelon" , "Pear" , "Grape" , "Pineapple" , "Strawberry" , "Cherry" , "Mango" )
封装到 adapter 中
val adapter = ArrayAdapter<String>(this , android.R.layout.simple_list_item_1, data )
泛型通过数据类型指定为 String
Context 参数为当前 Activity
resource: 参数指的是数据项的布局 id, 这里我们使用内置的 simple_list_item_1
设置 adapter 到 listview 中
list_main.adapter = adapter
adapter 适配器, 是数据项与视图(展示)的桥梁, 即 MVC 中的 C(Controller): An Adapter object acts as a bridge between an AdapterView and the underlying data for that view. The Adapter provides access to the data items. The Adapter is also responsible for making a View for each item in the data set.
常用有 ArrayAdapter
, CursorAdapter
, SimpleCursorAdapter
不同的类定义了不同的数据展示形式
自定义 adapter 主要是自定义数据项的显示界面
首先对我们的数据项中的数据进行封装
class Fruit (val name:String, val imageId: Int ) {}
然后编写数据项的 Layout (作为 Adapter 的参数)
1 2 3 4 5 6 7 8 <LinearLayout ... > <ImageView ... /> <TextView ... /> </LinearLayout >
自定义 Adapter: 在 ArrayAdapter 的基础上自定义展示视图
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 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 image_fruit_item: ImageView = view.findViewById(R.id.image_fruit_item) val text_fruit_name:TextView = view.findViewById(R.id.text_fruit_name) viewHolder = ViewHolder((image_fruit_item, text_fruit_name)) 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 } }
Apply Adapter to data
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 private val fruitList = ArrayList<Fruit>() override fun onCreate (savedInstanceState: Bundle ?) { super .onCreate(savedInstanceState) setContentView(R.layout.activity_main) supportActionBar?.hide() val list_main = findViewById<ListView>(R.id.list_main) initFruits() val adapter = FruitAdapter(this , R.layout.fruit_item, fruitList) list_main.adapter = adapter } private fun initFruits () { repeat(2 ) { fruitList.add(Fruit("Cherry" , R.drawable.cherry_pic)) fruitList.add(Fruit("Mango" , R.drawable.mango_pic)) ... } }
Adapter 性能优化过程 这是最初的 Adapter 编写思路:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 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 image_fruit_item: ImageView = view.findViewById(R.id.image_fruit_item) val text_fruit_name:TextView = view.findViewById(R.id.text_fruit_name) val fruit = getItem(position) if (fruit != null ) { image_fruit_item.setImageResource(fruit.imageId) text_fruit_name.text = fruit.name } return view } }
Tips: getView 默认每次调用时都会将数据项布局重新加载一遍, 因此当快速滑动时将会对性能有所影响, 但是每个数据项的布局基本是一致的, 因此我们可以借助对之前布局的缓存 convertView
进行优化
1 2 3 4 5 6 7 val view: Viewif (convertView == null ) { view = LayoutInflater.from(context).inflate(resourceId, parent, false ) } else { view = convertView }
Tips: 继续观察代码我们发现针对布局中的控件我们每次还是需要 find
, 可以借助 ViewHolder
对这部分性能进行优化
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 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 image_fruit_item: ImageView = view.findViewById(R.id.image_fruit_item) val text_fruit_name:TextView = view.findViewById(R.id.text_fruit_name) viewHolder = ViewHolder((image_fruit_item, text_fruit_name)) 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 }
点击事件 ListView 的点击事件会冒泡到 ListView 对象上进行统一处理:
1 2 3 4 5 6 7 8 9 list_main.setOnItemClickListener { adapterView, view, i, l -> val fruitItem = fruitList[i] Toast.makeText(this , fruitItem.name, Toast.LENGTH_SHORT).show() } list_main.setOnItemClickListener { _, _, i, _ -> val fruitItem = fruitList[i] Toast.makeText(this , fruitItem.name, Toast.LENGTH_SHORT).show() }
RecyclerView (Why) ListView 在性能, 扩展性等方面存在限制, 因此 Android 提供了更强大的 RecycleView 替代并增强了 ListView(不删除是为了向下适应老版本)
Quick Start
准备好数据项的布局
创建 Adapter 类:实现 ViewHoder 的声明与定义, 子项的赋值等基本功能
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 class RecyclerFruitAdapter (val fruitList: List<Fruit>) : RecyclerView.Adapter<RecyclerFruitAdapter.ViewHolder>() { inner class ViewHolder (view: View) : RecyclerView.ViewHolder(view) { val fruitImage: ImageView = view.findViewById(R.id.image_fruit_item) val fruitName: TextView = view.findViewById(R.id.text_fruit_name) } 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 指定父布局和 adapter
1 2 3 4 5 val recyclerView = findViewById<RecyclerView>(R.id.recyclerView)val layoutManager = LinearLayoutManager(this )recyclerView.layoutManager = layoutManager val adapter = RecyclerFruitAdapter(fruitList)recyclerView.adapter = adapter
扩展布局 水平排列
首先设置好数据子项的布局,至少水平排列时不能占一满行
然后再去设置 RecyclerView 的父布局即可
val layoutManager = LinearLayoutManager(this )layoutManager.orientation = LinearLayoutManager.HORIZONTAL recyclerView.layoutManager = layoutManager
瀑布流布局
设置好子项的布局
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 <LinearLayout xmlns:android ="http://schemas.android.com/apk/res/android" android:orientation ="vertical" android:layout_width ="match_parent" android:layout_height ="wrap_content" android:layout_margin ="5dp" > <ImageView android:layout_width ="40dp" android:layout_height ="40dp" android:id ="@+id/image_fruit_item" android:layout_gravity ="center_horizontal" android:layout_marginTop ="10dp" /> <TextView android:layout_width ="wrap_content" android:layout_height ="wrap_content" android:id ="@+id/text_fruit_name" android:layout_gravity ="left" android:layout_marginTop ="10dp" /> </LinearLayout >
使用 StaggeredGridLayoutManager
val recyclerView = findViewById<RecyclerView>(R.id.recyclerView)val layoutManager = StaggeredGridLayoutManager(3 , StaggeredGridLayoutManager.VERTICAL)recyclerView.layoutManager = layoutManager
点击事件 Recycler 为了更精细的控制, 将点击事件的绑定精细到了每一个子项, 在生成子项布局 onCreateViewHolder
时进行定义
1 2 3 4 5 6 7 8 9 10 11 override fun onCreateViewHolder (parent: ViewGroup , viewType: Int ) : ViewHolder { val view = LayoutInflater.from(parent.context).inflate(R.layout.todoitem_card, parent, false ) val holder = ViewHolder(view) holder.itemView.setOnClickListener { val position = holder.adapterPosition val todoItem = todoItemList[position] startTodoItemInfo(todoItem.id, todoItem.name) } return holder }
滑动事件 这里以滑动删除事件为例进行简单介绍
首先要建立自己的类, 继承 ItemTouchHelper.XXCallback
这些回调方法, 并实现回调处理, 其中 onSwiped 就是滑动的检测; 两个参数分别为拖动的方向(一般实现两个组件间位置交互)与滑动(一般实现目标组件的交互)的方向, 0 表示不可拖动或滑动, 具体方向由 ItemTouchHelper 中的静态属性分配
1 2 3 4 5 6 7 8 9 10 11 12 13 14 class BookItemTouchHelperCallback (var adapter: RecyclerBookAdapter): ItemTouchHelper.SimpleCallback(0 , ItemTouchHelper.LEFT) { override fun onMove ( recyclerView: RecyclerView , viewHolder: RecyclerView .ViewHolder , target: RecyclerView .ViewHolder ) : Boolean { TODO("Not yet implemented" ) } override fun onSwiped (viewHolder: RecyclerView .ViewHolder , direction: Int ) { var pos = viewHolder.adapterPosition adapter.deleteItem(pos) } }
然后借助回调方法的实例创建 ItemTouchHelper
实例, 并绑定到目标 RecyclerView
上
val itemTouchHelper = ItemTouchHelper(BookItemTouchHelperCallback(adapter))itemTouchHelper.attachToRecyclerView(recycler)
Android 图标 应用程序的图标应该被分为两层:前景层和背景层
前景层用来展示应用图标的 Logo:
Mask 层: 在图标的前景层和背景层之间, 手机厂商负责定义
背景层用来衬托应用图标的 Logo: 只允许定义颜色和纹理,不能定义形状
res 目录处右击 new/Image_Asssert
修改前景的 Logo 图片与背景色
Android 签名文件 Android Studio 生成
Build/Generate Singed Bundle/APK
Android App Bundle 文件是用于上架 Google Play 商店的
APK for Android
填入 keystore 文件的路径和密码
Create New…
Validity 是 keystore 文件的有效时长,单位是年
Gradle 生成 [Ignore]
编辑 app/build.gradle 文件