自学记录-目录
自学记录,书籍参考《第一行代码Android》郭霖
Activity相关
手动创建布局
以项目名为ActivityTest为例,项目布局选择Add no Activity
1、在app/src/main/java/com.example.activitytest包右键New->Activity->Empty创建一个新的布局(FirstActivity),取消勾选Generate Layout File(自动创建布局文件)和Launcher Activity(设置为主activity),点击Finish完成创建
2、在app/src/main/res目录下新建一个layout目录,右键layout->
New->Layout resource file,输入布局文件名(first_layout),根目录默认,点击ok完成创建
3、在FirstAcitvity.kt中的override fun onCreate()方法中加入如下代码
setContentView(R.layout.first_layout)
4、在AndroidManifest.xml文件中注册,在< activity >元素中插入如下代码
<activity android:name=".FirstActivity" android:label="This is FirstActivity">
<intent-filter>
<action android:name="android.intent.action.MAIN"/>
<category android:name="android.intent.category.LAUNCHER"/>
</intent-filter>
</activity>
其中,".FirstActivity"由于前面有package声明而省略前面的部分,android:label部分会在该activity加载后出现在标题部分,< intent-filter >元素中的代码是在进行主Activity配置
创建按钮组件
继续使用上文的项目,在first_layout.xml中此时已经存在< LinearLayout >元素,此时在其中可插入如下代码
<Button
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:id="@+id/Button1"
android:text="Button1"
/>
其中,增加id使用@+id/id_name 如果是引用当前已有id则使用@id/id_name,match_parent参数代表布局的宽度对齐父元素,wrap_content参数代表布局的高度刚好能够包含当前内容
设置Toast提醒
Toast是Android提供的提醒方式,在程序中可以发送一条快速的信息提示并自动消失
继续使用上文的项目,在FirstAcitvity.kt中的override fun onCreate()方法中加入如下代码
val button1:Button=findViewById(R.id.Button1)
button1.setOnClickListener {
Toast.makeText(this,"you clicked Button1",Toast.LENGTH_SHORT).show()
}
在Avtivity中,通过findViewByID()方法获取在布局文件中的元素,传入R.id.Button1后获取了按钮的实例,但是由于推导机制无法自动推导控件类型,所以需要显式声明为Button类型
获得实例后,通过.setOnClickListener{}为按钮注册一个监听器,当点击按钮时会执行监听器中的onClick()方法即后面的{}中内容
makeText()方法需要三个参数,第一个参数是Context,是Toast要求的上下文,由于Activity本身是一个Context对象,因此传入this即可,第二个参数是内容,第三个参数是显示的时长,默认提供两个内置常量Toast.LENGTH_SHORT/LONG
在有多个组件的时候,若是每个组件都进行findViewById()的声明过于繁琐,因此有插件提供了支持(需要AS代码补全的功能协助),此时可以直接通过id使用该对象,即可删去上述代码的第一行
menu
菜单展示
1、在res目录下新建一个menu目录,再在该目录下新建一个Menu resource file(命名为main)
2、在< menu >元素中插入以下代码
<item
android:id="@+id/add_item"
android:title="add"
/>
<item
android:id="@+id/remove_item"
android:title="Remove"
/>
上述代码创建了两个菜单项,分别分配了唯一的id和指定的名称
3、回到FirstActivity.kt,重写onCreateOptionsMenu()方法(ctrl+o快速选择重写方法),重写代码如下
override fun onCreateOptionsMenu(menu: Menu?): Boolean {
menuInflater.inflate(R.menu.main,menu)
return true
}
由于在Kotlin中调用Java Bean的语法结构的方法时,被简化为可以直接使用对象引用做到getter/setter的效果,因此menuInflater实际上是调用了父类的getMenuInflater()方法获得对象,再使用该对象的inflate()方法,该方法接受两个参数,第一个参数是指定资源文件进行菜单创建,第二个参数是用于指定菜单项将添加到哪一个Menu对象当中
4、上述步骤完成了菜单的显示,但菜单还应该有相应的能力,因此继续在FirstActivity.kt继续编写代码如下
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 true
}
在onOptionsItemSelected()方法中通过调用item.itemId判断点击的是哪一个菜单项(实际上也是调用了getItemId()
删除布局
使用finish()做到和手机的back键一样的效果,退出当前的Activity
除此之外,可以使用finishActivity()结束之前启动的活动,finish使用样例如下
在first_layout.xml中创建一个Button元素命名为Exit的过程略,在FirstActivity.kt中加入如下代码即可
exit.setOnClickListener {
finish()
}
用Intent启动Activity
为了完成多Activity的应用,需要在Activity之间进行跳转,此时就用到了Intent
继续上面的项目,在activitytest包中创建SecondActivity,这次勾选Generate Layout File选项,让AS进行创建,创建后会自动生成SecondActivity.kt和layout_second.xml,为了方便接下来的操作(实际创建的情况和书上的内容略有差异稍作调整),将layout_second.xml重命名为second_layout.xml(修改后SecondActivity.kt中的布局名自动发生了更改),然后将second_layout.xml的内容替换为LinearLayout布局(复制first_layout.xml的内容),并定义按钮Button2,检查AndroidManifest.xml时会看到AS自动添加了新的< activity >元素
完成创建后,接下来就需要考虑如何启动的问题,此时引入Intent的概念,Intent是Android各组件进行交互的重要方式,可以在不同组件之中传递数据,当前仅研究如何启动Activity,大致上可以将Intent分为两种:显式Intent和隐式Intent
显式Intent
Intent有多个构造函数重载,其中之一是Intent(Context packageContext,Class<?> cls),第一个参数Context要求提供一个启动Activity的上下文,第二个参数用于指定启动的Activity,该方法显示了该Intent的“意图”,而其使用要搭配Activity类的startActivity()方法
修改Button1的代码如下
button1.setOnClickListener {
val intent=Intent(this,SecondActivity::class.java)
startActivity(intent)
}
上述代码即在FirstActivity的基础上打开SecondActivity,在不做其他设置的情况下返回FirstActivity直接使用手机的back键即可
由于上述方式启动Activity的过程中,Intent的"意图"十分明显,因此被称为显式Intent
*仿照上文在SecondActivity中创建一个Exit Button,编写好finish()后发现是销毁了当前的Activity而不是直接退出App
隐式Intent
隐式的Intent并不明确的指出启动哪一个Activity,而是通过指定一系列抽象的action和category信息,交由系统进行分析并决定启动合适的Activity
所谓合适的Activity,就是可以响应对应Intent的Activity,而为了要让成为对应的Activity,需要通过< activity >标签下配置< intent-filter >对action和category进行指定,打开AndroidManifest.xml,在
SecondActivity的< activity >中添加如下代码
<intent-filter>
<action android:name="com.example.activitytest.ACTION_START"/>
<category android:name="android.intent.category.DEFAULT"/>
</intent-filter>
上述代码中< action >指明了当前Activity可以响应com.example.activitytest.ACTION_START这个action,而< category >则更精确指明当前的Activity中可能带有的category,二者均满足时这个Activity才能响应该Intent
而后修改Button1的代码为
button1.setOnClickListener {
val intent=Intent("com.example.activitytest.ACTION_START")
startActivity(intent)
}
上述代码中使用了Intent的另一个构造函数,将action的字符串传入,至于为何只有一个参数,那是因为category设置为了默认DEFAULT,调用startActivity()方法时会进行自动填充
此时便完成了一次隐式Intent的编写
每个Intent只能指定一个action,但能指定多个category,目前的Intent中只有一个默认的category,此时可通过以下代码增加,修改FirstActivity.kt中的botton1
button1.setOnClickListener {
val intent=Intent("com.example.activitytest.ACTION_START")
intent.addCategory("com.example.activitytest.MY_CATEGORY")
startActivity(intent)
}
同时修改AndroidManifest.xml中的对应< category >
<category android:name="android.intent.category.MY_CATEGORY"/>
若是不对AndroidManifest.xml修改(缺少了对应的< category >)会导致程序崩溃(闪退)
隐式Intent的更多用法-启动其他程序的Activity
使用隐式Intent,不仅可以启动自己程序的Activity,还可以启动其他程序的Activity,比如在应用程序中如果需要展示一个网页,可以调用系统的浏览器的Activity来打开这个网页,修改Button1的代码如下
button1.setOnClickListener {
val intent=Intent(Intent.ACTION_VIEW)
intent.data= Uri.parse("https://www.baidu.com")
startActivity(intent)
}
首先指定了Intent的action是Intent.ACTION_VIEW,这个参数是Android内置的动作,常量值为android.intent.action.VIEW,然后通过Uri.parse()将网址字符串解析为Uri对象,再调用setData()方法将这个Uri对象传递过去(利用了上文加粗字体提到的Kotlin语法糖使得这里的操作像是在对data进行赋值)
此处提到的setData()方法在上文未提及,这个方法接收一个Uri对象,用于指定当前Intent正在操作的数据,与此对应,可以在< intent-filter >中再配置一个< data >标签用于更为精确的指定当前Activity能够响应的数据,< data >可配置的主要内容如下
参数 | 作用 |
---|---|
android:scheme | 用于指定数据的协议部分,如上例中的http |
android:host | 用于指定数据的主机名部分,如www.baidu.com |
android:port | 用于指定数据的端口部分,一般紧跟主机名后 |
android:path | 用于指定主机名和端口之后的部分,如一段网址跟在域名之后的内容 |
android:mimeType | 用于指定可以处理的数据类型,允许一段网址中跟在域名之后的内容 |
只有当< data >标签中指定的内容和Intent携带的Data完全一致时,当前Activity才能响应这个data
为了理解上述内容,新建第三个Activity,命名为ThirdActivity,在third_layout.xml中创建Button3,而后在AndroidManifest.xml中修改ThirdActivity的注册信息如下
<intent-filter tools:ignore="AppLinkUrlError">
<action android:name="android.intent.action.VIEW"/>
<category android:name="android.intent.category.DEFAULT"/>
<data android:scheme="https"/>
</intent-filter>
注:在输入书上代码后提示无法找到tool:ignore,而使用代码补全功能则不报错,检查后发现在导包的代码上方多了一行xmlns:tools="http://schemas.android.com/tools",
增加后不再报错,但是仍然没有出现可选的效果,点击Button1仍然直接弹出浏览器
除了https协议外,还能指定很多其他的协议,比如geo显示地理位置,tel拨打电话,拨打电话的代码如下
val intent=Intent(Intent.ACTION_DIAL)
intent.data=Uri.parse("tel:10086") //拨打10086
向下一个Activity传递数据
在Activity之间传递数据,即将数据暂存在Intent中,在切换Activity后重新取出即可,而Intent则为此提供了一系列putExtra()方法的重载,比如为了将字符串从FirstActivity传递到SecondActivity中,可以使用如下代码
button1.setOnClickListener {
val data="Hello Java"
val intent=Intent(this,SecondActivity::class.java)
intent.putExtra("extra_data",data)
startActivity(intent)
}
这里使用显式启动SecondActivity,并使用putExtra()方法传递了一个字符串,putExtra()方法接受两个参数,第一个参数是键,用于之后获取值,第二个参数是要传递的数据
然后在SecondActivity中将要传递的数据取出,代码如下
val extraData=intent.getStringExtra("extra_data")
Log.d("SecondActivity","extraData is $extraData")
上述代码实际上是调用父类的getIntent()方法,用于启动SecondActivity的Intent,然后调用getStringExtra()方法获取键值,这样就可以获取传递的数据了,如果传递的是其他类型的数据,则使用对应的getXXXExtra()方法即可
向上一个Activity传递数据
既然能向下传递数据,那么也应该可以向上传递数据,但是,由于返回上一个Activity只需要按一下Back就返回了,因此没有Intent用于传递,解决方法来自于Activity类中的startActivityForResult()方法,这个方法期望在Activity销毁的时候能返回一个结果到上一个Activity,这个方法接受两个参数,第一个参数为Intent,第二个参数是请求码,用来判断数据的来源,实现代码如下
首先修改Button1的代码
button1.setOnClickListener {
val intent=Intent(this,SecondActivity::class.java)
startActivityForResult(intent,1)
}
请求码为唯一值即可,接下来修改Button2的代码
button2.setOnClickListener {
val intent=Intent()
intent.putExtra("data_return","hello java")
setResult(RESULT_OK,intent)
finish()
}
上述代码构建了一个Intent,不过这个Intent仅仅用于传输数据,通过setResult()方法进行向上传输数据,该方法接收两个参数,第一个参数用于向上返回结果,一般只使用RESULT_OK和RESULT_CANCELED两个值,第二个参数则是传回带有数据的Intent,最后使用finish()销毁Activity
由于是使用startActivityForResult()方法启动的SecondActivity的,在其被销毁后会调回上一个Activity的onActivityResult()方法,因此需要重写该方法获取数据,代码如下
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
super.onActivityResult(requestCode, resultCode, data)
when(requestCode){
1->if(resultCode==RESULT_OK){
val returnedData =data?.getStringExtra("data_return")
Log.d("FirstActivity","returned data is $returnedData")
}
}
}
第一个参数requestCode为启动Activity的时候的请求码,第二个参数是在返回数据时的处理结果,第三个参数即携带数据的Intent。由于在一个Activity中会有可能调用startActivityForResult()去启动很多不同的Activity,每一个数据的返回都会回调到onActivityResult中,因此需要检查requestCode的值来判断来源以及通过resultCode判断数据传输是否成功,最后获取数据
除此之外,用户可能还会通过back键来结束activity,因此,在SecondActivity中重写onBackPressed()来规避也是有必要的,代码如下
override fun onBackPressed() {
val intent= Intent()
intent.putExtra("data_return","hello java with BackPressed")
setResult(RESULT_OK,intent)
finish()
}
这样,无论是通过哪种方式返回上一个Activity,信息都会被正常返回
防止由于Activity被回收而导致的文本遗失
由于一个Activity进入了停止状态后,系统在内存不足的情况下有可能会将该Activity回收,此时如果返回该Activity,该Activity会被正常显示,但此时调用的方法不再是onRestart()而是onCreate(),但是这种情况下临时的数据状态会遗失,假设此时Activity中存在一个有信息的文本输入框,则该输入框的内容会因为这种情况而遗失
为了避免这种问题,Activity提供了一个onSaveInstanceState()回调方法,该方法保证在Activity回收之前被调用,这个方法会提供一个Bundle类型的参数,而Bundle提供一系列的方法保存数据,比如putString(),putInt()等等,这些方法需要两个参数,第一个参数是键,用于后续取值,而第二个参数则是需要保存的内容
实际运用如下
//在前一个Activity
if(savedInstanceState!=null){
val tempData=savedInstanceState.getString("data_key")
Log.d("FirstActivity", tempData.toString())
}
//在后一个Activity
override fun onSaveInstanceState(outState: Bundle) {
super.onSaveInstanceState(outState)
val tempData="Something you just typed"
outState.putString("data_key",tempData)
}
//效果未检验
Activity的启动模式
standard
默认启动模式,在AndroidManiFest.xml中添加如下代码进行手动指定
<activity
android:launchMode="standard">
若是通过自身调用重复创建某Activity,则每次都会创建不同的本实例,创建次数和退出时返回次数相等
singleTop
当该Activity处于返回栈顶时,进行自身调用创建Activity都会直接使用该Activity实例,返回次数仅为1,而并非处于返回栈顶时则会重复创建新的实例,效果与standard相同
singleTask
无论该Activity是否处于返回栈顶,进行自身调用创建Activity都会直接使用该Activity实例
singleInstance
是Activity的启动模式中最复杂的一个,现在有FirstAcitvity/SecondActivity/ThirdActivity,其中SecondActivity的启动模式为singleInstance,此时依次进行启动,会发现SecondActivity的taskId和另外两个不同,此时从ThirdActivity按back依次返回,返回的顺序是FirstAcitvity/ThirdActivity/SecondActivity,这是因为SecondActivity被放入另外一个返回栈中保存,此时程序从ThirdActivity开始依次返回,紧接着弹出的是FirstActivity,在该栈清空后才会弹出处于另一个栈的SecondActivity
Activity最佳实践
知晓此时处于哪一个Activity
1、创建一个BaseActivity并令其继承AppCompatActivity,且重写onCreate()方法如下
class BaseActivity:AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
Log.d("BaseActivity",javaClass.simpleName)
}
}
这个类不需要在AndroidManifest.xml中注册
2、将BaseActivity用open修饰,并将所有Activity的继承改为继承至BaseActivity
3、增加过滤器BaseActivity
此时每次进入一个Activity中的界面,该Activity的类名就会被打印出来
随时随地退出程序
为了能够一键退出程序,只需要用一个专门的集合对所有Activity进行管理即可
1、新建单例类ActivityCollector,修改代码如下
object ActivityCollector {
private val activitys= ArrayList<Activity>()
fun addActivity(activity: Activity){
activitys.add(activity)
}
fun removeActivity(activity: Activity){
activitys.remove(activity)
}
fun finishAll(){
for(activity in activitys){
if(!activity.isFinishing){
activity.finish()
}
}
activitys.clear()
}
}
2、修改BaseActivity中的代码
open class BaseActivity:AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
Log.d("BaseActivity",javaClass.simpleName)
ActivityCollector.addActivity(this)
}
override fun onDestroy() {
super.onDestroy()
ActivityCollector.removeActivity(this)
}
}
3、在需要进行一键退出的组件聆听器中调用ActivityCollector.finishAll()即可
4、若是为了保证程序完全退出,可以在finishAll()的最后加上android.os.Process.killProcess(android.os.Process.myPid())
,该方法只能用于杀掉当前程序的进程,并不能杀掉其他程序的进程