【7】应用开发——Activity全面讲解

提示:此文章仅作为本人记录日常学习使用,若有存在错误或者不严谨得地方,欢迎各位在评论中指出。

一、Activity

1.1 Activity基础

1.1.1 手动创建和加载布局

Android Studio会在创建一个新Activity时自动帮我们生成相应的layout布局文件并在Activity内引用这个布局。但是我们这次选择手动创建和加载布局。
创建first_layout.xml布局文件:

<?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="match_parent"
    android:orientation="vertical">
    
    <!-- 添加一个按钮Button -->
    <Button
        android:id="@+id/button1"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:text="Button 1" />
        
</LinearLayout>

在Activity中手动加载这个布局:

class FirstActivity : AppCompatActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        //引用相应的布局
        setContentView(R.layout.first_layout)
    }

在这里调用了 setContentView() 方法,并传入一个first_layout.xml布局文件的id作为参数,这样就可以给FirstActivity加载一个布局了。

在Android项目中,R是一个特殊的类,用于引用所有的资源,如布局(layout)、图片(drawable)、字符串(string)等,我们在项目中添加的任何资源都会在R文件中生成一个相应的资源id

1.1.2 在AndroidManifest文件中注册

在程序运行前,我们需要告诉应用程序应该先启动哪个Activity。下面是如何为程序配置主Activity的示例代码:

<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools">

    <application
        ···>
        <activity
            android:name=".FirstActivity"
            android:label="This is First Activity!" // 指定Activity标题栏内容
            android:exported="true">
            // Begin 为程序配置主Activity
            <intent-filter>
                <action android:name="android.intent.action.MAIN" />
                <category android:name="android.intent.category.LAUNCHER" />
            </intent-filter>
            // End 为程序配置主Activity
        </activity>
    </application>

</manifest>

这样FirstActivity就是我们这个程序的主Activity了,当我们打开App时最先显示的就是这个Activity

1.1.2 在Activity中添加Toast(吐司弹窗)

findViewById():返回一个继承自View的泛型对象,所以Kotlin无法推导出该对象是什么控件,所以我们需要显式的将变量声明成Button类型。
makeText():通过makeText()方法 可以创建一个Toast对象,然后调用show()方法将Toast显示出来。makeText()方法需要传入三个参数:①Context上下文②要显示的内容③显示的时长。

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.first_layout)
        //为按钮绑定view控件
        val button1: Button = findViewById(R.id.button1)
        //为按钮设置点击触发事件
        button1.setOnClickListener {
                         上下文     Toast要显示的内容        显示时长       记得调用show()不然Toast无法显示
            Toast.makeText(this, "You clicked Button 1", Toast.LENGTH_SHORT).show()
        }
    }

1.1.2 在Activity中使用Menu(菜单)

在res目录下新建一个menu文件夹:res—>New—>Directory—>输入文件名menu—>点击OK
在menu文件夹下新建一个main菜单文件:menu—>New—>Menu resource file—>输入main—>点击OK

<?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>

在这里插入图片描述

Android Studio中重写的快捷键是Ctrl + O
在这里插入图片描述
我们在FirstActivity中重写了onCreateOptionsMenu() 方法,这里面menuInflater实际上是调用了父类的getMenuInflater方法得到一个MenuInflater对象,然后再调用它的inflate()方法,就可以给当前Activity创建菜单了。inflate()方法需要接收两个参数:①通过哪个资源文件创建菜单 ②将菜单添加到哪个Menu对象
Kotlin Code:

override fun onCreateOptionsMenu(menu: Menu?): Boolean {
                       menu资源文件   menu对象
    menuInflater.inflate(R.menu.main, menu)
    // 是否显示菜单
    return true
}

Java Code:

@Override
public boolean onCreateOptionsMenu(Menu menu) {
    getMenuInflater().inflate(R.menu.main, menu);
    return true;
}

接下来就可以自定义菜单的响应事件了,重写 onOptionsItemSelected() 方法来实现:
Kotlin Code:

override fun onOptionsItemSelected(item: MenuItem): Boolean {
    when (item.itemId) {
                                                                              调用show()显示Toast
        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
}

Java Code:

    @Override
    public boolean onOptionsItemSelected(MenuItem item) {
        if (item.getItemId() == R.id.add_item) {
            Toast.makeText(this, "You clicked Add.", Toast.LENGTH_SHORT).show();
            return true;
        } else if (item.getItemId() == R.id.remove_item) {
            Toast.makeText(this, "You clicked Remove.", Toast.LENGTH_SHORT).show();
            return true;
        } else {
            return super.onOptionsItemSelected(item);
        }
    }

1.2 使用Intent实现页面的跳转

Intent是Android程序中各组件之间进行交互的一种重要方式。它不仅可以指明当前组件想要执行的动作,还可以在不同组件之间传递数据。Intent又分为两种:

  • 显式Intent:指明目标组件,并将其完全限定名作为参数
  • 隐式Intent:没有指明目标组件,而是通过cation + category来进行匹配,让系统选择合适的组件进行处理

1.2.1 显式Intent的使用

我们先来学习一下显式Intent,我们这次让Android Studio帮我们新建一个SecondActivity(自动生成layout并在AndroidManifest中注册):

<?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="match_parent"
    android:orientation="vertical">

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

</LinearLayout>

像下面这种,直接表明我们想要从当前Activity跳转到SecondActivity的Intent就叫做显式Intent,因为我们直接显式的声明了我们想要从A页面跳转到B页面。接下来,我们为FirstActivity中的按钮添加点击事件:
Kotlin Code:

override fun onCreate(savedInstanceState: Bundle?) {
    super.onCreate(savedInstanceState)
    setContentView(R.layout.first_layout)
    // button1的点击事件
    button1.setOnClickListener {
                            上下文      要启动的目标Activity
        val intent = Intent(this, SecondActivity::class.java)
        startActivity(intent)
    }
}

Java Code:

@Override
protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    mainBinding = ActivityMainBinding.inflate(getLayoutInflater());
    setContentView(mainBinding.getRoot());
    mainBinding.button1.setOnClickListener(new View.OnClickListener() {
        @Override
        public void onClick(View v) {
            Intent intent = new Intent(MainActivity.this, SecondActivity.class);
            startActivity(intent);
        }
    });
}

在上面的代码中我们声明了一个Intent对象,Intent对象要求传入两个参数:①上下文和②目标Activity。这里面Kotlin语言的SecondActivity::class.java相当于Java语言中的SecondActivity.class。然后我们通过startActivity()方法来实现页面的跳转,并向其中传入intent参数。

1.2.2 隐式Intent的使用

相比于显式Intent,隐式Intent就要含蓄的多。隐式Intent并没有明确指明目标组件,而是通过actioncategory等信息来让系统判断要启动哪个Activity。需要注意的是,使用隐式Intent时需要确保至少存在一个能够处理该Intent的组件,否则会导致应用崩溃
在这里插入图片描述
AndroidManifest.xml中给SecondActivity配置隐式Intent的方法:

  • < action > 标签 中指明当前Activity可以响应“com.example.activitytest.ACTION_START”这个action。
  • < category >标签 中指明该Intent除了action还必须带有"android.intent.category.DEFAULT"的category才可以响应。

注意:只有Activity的< action >和< category >中的内容同时匹配Intent中指定的action和category时,这个Activity才能响应该Intent。

<application
    · · ·>
    <activity
        android:name=".SecondActivity"
        android:exported="true" //注意这里必须为true!
        android:label="This is SecondActivity">
        <intent-filter>
            //只有Activity中的action和category都和Intent匹配成功才能响应
            <action android:name="com.example.activitytest.ACTION_START" />
            <category android:name="android.intent.category.DEFAULT" />
        </intent-filter>
    </activity>

FirstActivity.java中修改按钮点击逻辑:
Kotlin Code:

override fun onCreate(savedInstanceState: Bundle?) {
    super.onCreate(savedInstanceState)
    setContentView(R.layout.first_layout)
    button1.setOnClickListener {
        //为当前intent配置action
        val intent = Intent("com.example.activitytest.ACTION_START")
        startActivity(intent)
    }

Java Code:

@Override
protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    mainBinding = ActivityMainBinding.inflate(getLayoutInflater());
    setContentView(mainBinding.getRoot());
    mainBinding.button1.setOnClickListener(new View.OnClickListener() {
        @Override
        public void onClick(View v) {
            Intent intent = new Intent("com.dolby.myapplication.ACTION_START");
            startActivity(intent);
        }
    });
}

这次你点击FirstActivity中的Button按钮,实现跳转到SecondActivity页面就是通过隐式Intent来实现的。一个Intent只能配置一个action,但是却可以配置多个category。我们在上面的例子中使用了默认的category:“android.intent.category.DEFAULT”。下面我们额外增加一个category,看一下会发生什么:
Kotlin Code:

override fun onCreate(savedInstanceState: Bundle?) {
   super.onCreate(savedInstanceState)
   setContentView(R.layout.first_layout)
   button1.setOnClickListener {
       //该intent配置了一个ACTION_START的action和一个MY_CATEGORY的category
       val intent = Intent("com.example.activitytest.ACTION_START")
       intent.addCategory("com.example.activitytest.MY_CATEGORY")
       startActivity(intent)
   }

Java Code:

@Override
protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    mainBinding = ActivityMainBinding.inflate(getLayoutInflater());
    setContentView(mainBinding.getRoot());
    mainBinding.button1.setOnClickListener(new View.OnClickListener() {
        @Override
        public void onClick(View v) {
            Intent intent = new Intent("com.dolby.myapplication.ACTION_START");
            intent.addCategory("com.dolby.myapplication.MY_CATEGORY");
            startActivity(intent);
        }
    });
}

这时如果你点击FirstActivity中的Button1按钮并不会实现跳转,而是会导致crash。这是因为我们刚才在Intent中添加了一个自定义的category:“com.example.activitytest.MY_CATEGORY”。而SecondActivity中的< intent-filter >标签中没有声明可以响应这个自定义的category:

<intent-filter>
        <action android:name="com.example.activitytest.ACTION_START" />
        <category android:name="android.intent.category.DEFAULT" />
 </intent-filter>

在前面我们讲过,Activity中的action和category必须都和Intent中的action和category匹配成功,Activity才能响应这个Intent。这里只成功匹配了action,但是却没有可以与之相匹配的category,所以会导致程序crash。现在我们为SecondActivity配置这个自定义的category:

<intent-filter>
        <action android:name="com.example.activitytest.ACTION_START" />
        <category android:name="android.intent.category.DEFAULT" />
        //为SecondActivity添加一个category声明
        <category android:name="com.example.activitytest.MY_CATEGORY" />
 </intent-filter>

这次SecondActivity就可以响应action(ACTION_START)和category(DEFAULT/MY_CATEGORY)了。当我们再次点击FirstActivity中的Button1按钮时就可以成功跳转到SecondActivity了。

1.2.3 更多隐式Intent的用法

隐式Intent不仅可以启动自己程序内的Activity,还可以启动其他程序的Activity。接下来我们尝试调用系统的浏览器打开一个网页。修改FirstActivity中的按钮点击逻辑:
Kotlin Code:

button1.setOnClickListener {
    val intent = Intent(Intent.ACTION_VIEW)
    intent.data = Uri.parse("https://www.baidu.com")
    startActivity(intent)
}

Java Code:

mainBinding.button2.setOnClickListener(new View.OnClickListener() {
    @Override
    public void onClick(View v) {
        Intent intent = new Intent(Intent.ACTION_VIEW);
        intent.setData(Uri.parse("https://www.baidu.com"));
        startActivity(intent);
    }
});

这里我们指定这个Intent的action是“Intent.ACTION_VIEW”,并通过Uri.parse()将地址字符串解析成一个uri对象,然后将这个uri对象传递给Intent。这次在FirstActivity中点击Button1按钮,可以看到我们成功跳转到系统浏览器并打开百度了。

除了打开指定的页面,我们还可以在AndroidManifest文件中为Activity的< intent-filter>标签额外配置一个< data>标签用来更精准的指定当前Activity能够响应的数据。< data>标签中主要配置以下内容:

  • android:scheme:指定数据协议(例如:https)
  • android:host:指定数据主机名(例如:www.baidu.com)
  • android:port:指定数据端口
  • android:path:指定主机名和端口之后的部分(例如:域名后面的内容)
  • android:mimeType:指定可以处理的数据类型

①我们新建一个ThirdActivity.java文件用来实验< data >标签中的scheme配置。它的third_layout布局文件:

<?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="match_parent"
    android:orientation="vertical">
    <Button
        android:id="@+id/button3"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:text="Button 3" />
</LinearLayout>

在AndroidManifest.xml中:

<activity
    android:name=".ThirdActivity"
    android:exported="false"
    tools:ignore="AppLinkUrlError">
    <intent-filter>
        <action android:name="android.intent.action.VIEW" />
        <category android:name="android.intent.category.DEFAULT" />
        //指定数据协议必须为"https"
        <data android:scheme="https" />
    </intent-filter>
</activity>

这次我们在FirstActivity中点击Button3时,系统就会弹出提示框要我们选择使用哪个App来打开百度网页。由于我们在清单文件中配置了ThirdActivity可以响应https网页请求的操作,所以我们的ActivityTest应用也会出现在选择框里供用户选择。(这里只是演示data标签的使用,实际上ThirdActivity并不具备可以响应网页请求的功能。)

②接下来我们看一下如何在我们的程序中调用系统拨号盘
Kotlin Code:

button1.setOnClickListener {
    val intent = Intent(Intent.ACTION_DIAL)
    intent.data = Uri.parse("tel:10086")
    startActivity(intent)
}

Java Code:

mainBinding.button3.setOnClickListener(new View.OnClickListener() {
    @Override
    public void onClick(View v) {
        Intent intent = new Intent(Intent.ACTION_DIAL);
        intent.setData(Uri.parse("tel:10086"));
        startActivity(intent);
    }
});

这样我们在点击FirstActivity的Button1按钮时就会调用系统默认的拨号盘并自动帮我们填入10086号码了。

1.3 使用Intent实现数据的传递

Intent不仅可以用来启动Activity,也可以在启动Activity的时候传递数据

1.3.1 向下一个Activity传递数据

Intent提供了一系列 putExtra() 方法的重载,使得我们可以将数据以键值对的形式暂存在Intent中,在启动另一个Activity后再将这些数据从Intent中取出来。例如我们想将FirstActivity中的data字符串传递给SecondActivity,就可以这么写:
Kotlin Code:

button1.setOnClickListener {
    //需要传递给SecondActivity的数据data
    val data = "Hello SecondActivity"
    val intent = Intent(this, SecondActivity::class.java)
    //将要传递的数据通过putExtra方法以键值对的形式暂存在Intent中
                       key       value
    intent.putExtra("extra_data", data)
    startActivity(intent)
}

Java Code:

mainBinding.button4.setOnClickListener(new View.OnClickListener() {
    @Override
    public void onClick(View v) {
        String mData = "Hello SecondActivity";
        Intent intent = new Intent(MainActivity.this, SecondActivity.class);
        intent.putExtra("extra_data", mData);
        startActivity(intent);
    }
});

接下来,我们需要在SecondActivity中取出FirstActivity传递过来的数据:
Kotlin Code:

class SecondActivity : AppCompatActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_second)
        //通过key来接收另一个Activity传递过来的数据
        val extraData = intent.getStringExtra("extra_data")
        Log.d("SecondActivity", "extra_data is : $extraData")
    }
}

Java Code:

public class SecondActivity extends AppCompatActivity {
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_second);
        String extraData = getIntent().getStringExtra("extra_data");
        Toast.makeText(this, "Data From MainActivity:" + extraData, Toast.LENGTH_SHORT).show();
    }
}

这次我们点击FirstActivity页面的button就会跳转到SecondActivity页面中,并且在打印出FirstActivity传递过来的字符串:
在这里插入图片描述
除了getStringExtra的方法,我们还可以是用getIntExtra、getBooleanExtra等方法来向Intent中保存不同类型的数据。

1.3.2 通过startActivityForResult()向上一个Activity返回数据

Intent不仅可以传递数据给下一个Activity,也可以返回数据给上一个Activity。返回数据给上一个页面有两种触发方式:

  • ①在Activity销毁时返回结果给上一个Activity
  • ②按下Back返回键时返回数据给上一个Activity

①在Activity销毁时返回结果给上一个Activity
Activity类中还有一个 startActivityForResult() 方法,它会在目标Activity销毁时返回一些数据给当前ActivitystartActivityForResult()方法接收两个参数:①Intent ②请求码(必须>=0,用于判断数据来源的)。 我们修改FirstActivity中的按钮的点击事件:
Kotlin Code:

button1.setOnClickListener {
    val intent = Intent(this, SecondActivity::class.java)
                                   请求码(标识跳转的目标Activity)
    startActivityForResult(intent, 1001)
}

Java Code:

mainBinding.button5.setOnClickListener(new View.OnClickListener() {
    @Override
    public void onClick(View v) {
        Intent intent = new Intent(MainActivity.this, SecondActivity.class);
        startActivityForResult(intent, 1001);
    }
});

接下来我们需要实现SecondActivity中按钮的点击事件:
SecondActivity中的这个Intent没有任何"意图",它的作用仅仅是用于传递数据。setResult()方法非常重要,专门用于向上一个Activity返回数据。setResult()方法接收两个参数:①向上一个Activity返回处理结果(RESULT_OK或RESULT_CANCELED) ②带有数据的Intent。
Kotlin Code:

button2.setOnClickListener {
    val intent = Intent()
    //向上一个Activity返回的数据
                        key                 value
    intent.putExtra("data_return", "Hello FirstActivity")
              处理结果   带有数据的intent
    setResult(RESULT_OK, intent)
    finish()
}

Java Code:

secondBinding.buttonReturnDataFinish.setOnClickListener(new View.OnClickListener() {
    @Override
    public void onClick(View v) {
        Intent intent = new Intent();
        intent.putExtra("data_return", "Hello FirstActivity");
        setResult(RESULT_OK, intent);
        finish();
    }
});

现在SecondActivity中点击Button3就能够销毁当前页面向FirstActivity传递数据了。由于我们是使用startActivityForResult()方法启动SecondActivity的,当SecondActivity被销毁后会调用上一个Activity的onActivityResult()方法。因此我们需要在FirstActivity中重写这个方法来得到返回的数据:
Kotlin Code:

                                请求码           处理结果        带有返回数据的Intent
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
    super.onActivityResult(requestCode, resultCode, data)
    //通过请求码确定返回的数据是否来自SecondActivity
    when (requestCode) {
        //若数据来自SecondActivity并且处理结果为OK
        1001 -> if (resultCode == RESULT_OK) {
            //将返回的数据保存至returnedData中
            val returnedData = data?.getStringExtra("data_return")
            Log.d("FirstActivity", "returned data is: $returnedData")
        }
    }
}

Java Code:

@Override
protected void onActivityResult(int requestCode, int resultCode, @Nullable Intent data) {
    super.onActivityResult(requestCode, resultCode, data);
    if (requestCode == 1001) {
        if (resultCode == RESULT_OK) {
            String returnData = getIntent().getStringExtra("extra_data");
            Toast.makeText(this, "SecondActivity已经被销毁,他返回来的数据是:" + returnData, Toast.LENGTH_SHORT).show();
        }
    }
}

在这里插入图片描述
按下Back返回键时返回数据给上一个Activity
在上面的例子中我们是通过在SecondActivity中点击Button2按钮来触发页面销毁并返回FirstActivity的。那么当用户点击Back返回按钮返回到FirstActivity时该怎么返回数据给FirstActivity呢?我们可以通过在SecondActivity中重写onBackPressed()方法来解决:
Kotlin Code:

override fun onBackPressed() {
    val intent = Intent()
    intent.putExtra("data_return", "Hello FirstActivity")
    setResult(RESULT_OK, intent)
    finish()
}

Java Code:

onMyBackPressed(true, new Runnable() {
    @Override
    public void run() {
        Intent intent = new Intent();
        intent.putExtra("data_return", "Hello FirstActivity");
        setResult(RESULT_OK, intent);
        finish();
    }
});

这样当用户按下Back按钮就会执行onBackPressed()方法中的代码,我们在这里添加返回数据的逻辑就可以了。

注意:由于onBackPressed()方法已经过时,可以使用getOnBackPressedDispatcher()来替代。关于如何使用getOnBackPressedDispatcher()方法,请参考我的这篇文章:https://blog.csdn.net/weixin_44570190/article/details/135995654

我们可以通过startActivityForResult()方法打开另一个Activity,并在另一个Activity销毁时返回一些数据给上一个Activity。遗憾的是startActivityForResult()方法在低版本Android上已经被弃用,转而用registerForActivityResult()方法进行替代。 关于如何使用registerForActivityResult(),请参考我的这篇文章:https://blog.csdn.net/weixin_44570190/article/details/135997627


2024年2月2日更新:

1.3.3 通过registerForActivityResult()向上一个Activity返回数据

①在Activity销毁时返回结果给上一个Activity
由于startActivityForResult()方法在高版本Android上被弃用了,所以我们来学习一下如何使用他的替代方法。下面的示例中使用到了ViewBinding来绑定控件,如果你还没使用过ViewBinding可以参考我的第23篇文章
我们期望从MainActivity——>SecondActivity后,销毁SecondActivity并返回数据给MainActivity。下面是MainActivity.kt中的代码:

class MainActivity : AppCompatActivity() {

    

private lateinit var mBinding: ActivityMainBinding

    private val requestDataLauncher =
        //注册ActivityResult的观察者 当启动的Activity返回结果时 这个观察者会被触发
        registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { result ->
            if (result.resultCode == RESULT_OK) {
                val data = result.data?.getStringExtra("mData")
                Toast.makeText(this,data,Toast.LENGTH_SHORT).show()
            }
        }

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        mBinding = ActivityMainBinding.inflate(layoutInflater)
        setContentView(mBinding.root)
        mBinding.mButton1.setOnClickListener {
            val intent = Intent(this, ThirdActivity::class.java)
            //通过ActivityResult观察者启动一个Activity 并在其销毁后返回结果
            requestDataLauncher.launch(intent)
        }
    }
}

下面是SecondActivity中的代码:

class ThirdActivity : AppCompatActivity() {

    private lateinit var mBinding: ActivityThirdBinding
    
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        mBinding = ActivityThirdBinding.inflate(layoutInflater)
        setContentView(mBinding.root)
        mBinding.mButton2.setOnClickListener {
            val data = "This is data for MainActivity!"
            val intent = Intent()
            // 将数据存放到intent中
            intent.putExtra("mData", data)
            setResult(RESULT_OK, intent)
            finish()
        }
    }
}

这样当我们在SecondActivity中点击按钮finish当前Activity时,就会将数据返回给MainActivity。
按下Back返回键时返回数据给上一个Activity
在旧版本中我们是通过重写onBackPressed()方法来实现返回按键的监听的,但是在Android 13版本中onBackPressed()方法被弃用了,我们可以使用AndroidX 的API来实现滑动返回手势功能的回调处理。
首先在build.gradle.kts文件中中添加以下依赖:

dependencies {
    implementation("androidx.activity:activity-ktx:1.9.0-alpha02")
    · · ·
}

然后在AndroidManifest.xml清单文件中将enableOnBackInvokedCallback属性设置为true:

<application
	 android:enableOnBackInvokedCallback="true"
	 · · ·
</application>

最后注册OnBackPressedCallback()方法来处理返回手势:

class SecondActivity: AppCompatActivity() {

    private lateinit var mBinding: ActivityThirdBinding
    
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        mBinding = ActivityThirdBinding.inflate(layoutInflater)
        setContentView(mBinding.root)
        
		// 在Activity中处理返回手势
        onBackPressed(true){
            val data = "This is data for MainActivity!"
            val intent = Intent()
            intent.putExtra("mData", data)
            setResult(RESULT_OK, intent)
            finish()
        }
    }
}

fun AppCompatActivity.onBackPressed(isEnabled: Boolean, callback: () -> Unit) {
    onBackPressedDispatcher.addCallback(this, object : OnBackPressedCallback(isEnabled) {
        override fun handleOnBackPressed() {
            callback()
        }
    })
}

这样当用户按下Back按钮就会执行onBackPressed()方法中的代码,我们在这里添加返回数据的逻辑就可以了。


1.4 Activity的生命周期

1.4.1 返回栈

在Android中Activity是可以层叠的:

  • 每当我们启动一个新的Activity,它就会在返回栈中入栈,并处于栈顶(覆盖在原来的Activity上)。
  • 每当我们点击Back键或者调用finish()方法去销毁一个Activity时,处于栈顶的Activity就会出栈,下面的Activity就会重新处于栈顶位置并显示出来。

1.4.2 Activity的生命周期

  • onCreate( ):Activity第一次被创建时调用(例如:加载布局、绑定事件)。
  • onStart( ):Activity由不可见变为可见时调用,此时Activity不可交互(位于后台并未获得焦点,界面尚未被绘制)。
  • onResume( ):Activity 准备好和用户进行交互 时调用,此时Activity可见(位于栈顶)并且可交互。
  • onPause( ):另一个Activity(未占满屏幕或者全透明)跑到前台时,原来Activity的onPause( )方法会被调用,此时原来的Activity不可见。
  • onStop( ):Activity完全不可见时调用。
  • onDestroy( ):Activity被销毁之前调用,之后Activity将变为销毁状态。
  • onRestart( ):Activity被重新启动时调用。
    在这里插入图片描述

1.4.3 Activity被回收了怎么办

想象以下场景:应用中有一个页面A,用户在页面A的基础上启动了页面B,此时页面A就进入了停止状态。若这个时候系统内存不足,就很可能将页面A回收掉了。当用户按下Back键返回页面A时,其实还是会正常显示页面A的。但是会重新执行页面A的onCreate()方法而不是onRestart()方法,因为页面A在被回收后会被重新创建一次。需要注意的是,页面A中可能存在临时数据(例如文本框中已经输入的文字)。在上面的情况下,当用户按下Back键返回页面A时会发现页面A中的临时数据(例如文本框中已经输入的文字)都没了,这可太糟糕了!

为此,Activity专门提供了一个 onSaveInstanceState()方法 来解决这个问题。onSaveInstanceState()方法可以保证在页面被回收之前一定会被调用,因此我们可以通过这个方法来解决临时数据的保存问题。onSaveInstanceState()方法携带一个Bundle类型的参数,我们可以用putString()、putInt()等方法将数据以键值对的形式保存到Bundle中
例如:MainActivity——>NormalActivity(此时MainActivity被回收、临时数据丢失)——>MainActivity。我们就可以在MainActivity中添加以下代码来保存当前页面中的临时数据:

override fun onSaveInstanceState(outState: Bundle) {
    super.onSaveInstanceState(outState)
    val tempData = "Something you wish to save"
                         key        value
    outState.putString("data_key", tempData)
}

这样就将临时数据保存下来了,那么该如何恢复呢?Activity的onCreate()方法其实也有一个Bundle类型的参数:

override fun onCreate(savedInstanceState: Bundle?) {···}

这个Bundle参数一般情况下都是null。但是如果你在Activity被回收之前,通过onSaveInstanceState()方法保存了数据,那么这个Bundle参数就会带有之前保存的全部数据。为了恢复MainActivity页面中的数据,我们只需要将数据提取出来即可:

override fun onCreate(savedInstanceState: Bundle?) {
    super.onCreate(savedInstanceState)
    setContentView(R.layout.activity_main)
    if (savedInstanceState != null) {
        //通过key将保存的临时数据取出
        val tempData = savedInstanceState.getString("data_key")
        Log.d("MainActivity", "tempData is :$tempData")
    }
}

取出值后再做相应的恢复操作就可以啦,比如说将文本内容再重新赋值给文本输入框等等,这里只是简单的打印一下。

1.5 Activity的启动模式

在Kotlin中,Activity的启动模式决定了Activity与返回栈之间的关联方式。Activity的启动模式一共有四种:① standard模式 、② singleTop模式 、③ singleTask模式、 ④ singleInstance模式。我们可以在AndroidManifest.xml文件中通过给< activity>标签指定android:launchMode=”XXX”来选择启动模式

1.5.1 standard模式(标准)

standard模式是Activity的默认启动方式。在standard模式下,每当启动一个新的Activity,他就会在返回栈中入栈,并且处于栈顶的位置。例如我们在A页面中有一个按钮,当我们点击这个按钮时会再次打开一个A页面。如果我们点击3次按钮,那么就会创建出三个新的A页面实例(实例标识是不一样的)。此时我们需要连续按3次Back键才能退出程序。

1.5.2 singleTop模式(栈顶复用)

singleTop模式下,若启动Activity时发现该Activity已经位于返回栈的栈顶,则认为可以直接复用它,不会再创建新的Activity实例

<activity
    android:name=".SingleTopActivity"
    //singleTop模式
    android:launchMode="singleTop"
    android:label="This is SingleTopActivity">
    <intent-filter>
        <action android:name="android.intent.action.MAIN" />
        <category android:name="android.intent.category.LAUNCHER" />
    </intent-filter>
</activity>

这次如果我们重复之前的动作,不管我们点击多少次按钮,都不会再创建新的Activity实例。因为当前A页面已经处于返回栈的栈顶,每当再次启动一个A页面时,都会直接使用处于栈顶的Activity。因此A页面只会有一个实例,我们只需要按一次Back返回键就能退出程序。
不过,若我们由A页面跳转到B页面,然后再由B页面跳转到A页面。这时栈顶发生了变化(A1——>B——>A2),所以这两个A页面的实例是两个完全不同的实例。当你按下Back返回键,会从A2页面回到B页面,再按一次Back返回键会从B页面回到A1页面,再次按下Back键才会退出程序(A2——>B——>A1——>Exit)。

1.5.3 singleTask模式(栈内复用)

在启动新的Activity时,系统会先在返回栈中查找是否已经存在该Activity的实例

  • 如果存在,则直接使用该Activity实例,并把在这个Activity之上的所有其他Activity统统出栈
  • 如果不存在,就会创建一个新的Activity实例
<activity
    android:name=".SingleTaskActivity"
    //singleTask模式
    android:launchMode="singleTask">
    ···
</activity>

如果我们还是由A页面跳转到B页面,然后再由B页面跳转到A页面,这时栈顶的状态是:A1——>B——>A1。当我们在B页面中启动A页面时,会发现返回栈中已经存在一个A页面的实例,并且是位于B页面之下。这时B页面会从返回栈中出栈,而A页面会重新成为栈顶。此时A页面会调用onRestart()方法,B页面会调用onDestroy()方法。
在这里插入图片描述

1.5.4 singleInstance模式(单实例)

singleInstance模式不同于以上三种模式。系统会为这种模式的Activity创建一个新的返回栈来管理这个Activity,并且这个返回栈中只会有这个Activity的实例。这么做的意义是什么呢?
设想一下,我们的程序中有一个Activity是允许其他程序调用的,如果想实现其他程序和我们的程序可以共享这个Activity的实例,用前面三种模式是没办法实现的。由于每个应用程序都有自己的返回栈,在前面三种模式的情况下,同一个Activity在不同的返回栈中入栈时必然创建了新的实例。
而使用singleInstance模式就可以解决这个问题。在这种模式下,系统会创建一个单独的返回栈来管理这个Activity。不管是哪个应用程序来访问这个Activity,都共用这个单独的返回栈,这样就解决了共享Activity实例的问题。

<activity
    android:name=".SingleInstanceActivity"
    //singleInstance模式
    android:launchMode="SingleInstanceActivity">
    ···
</activity>

我们来举个简单的例子,一共有A、B、C三个页面。我们只设置B页面的启动模式为singleInstance,然后进行页面跳转:A页面——>B页面——>C页面。这时你会发现,A页面的和C页面是存放在同一个返回栈中。由于B页面我们设置它的启动模式为singleInstance模式,所以B页面实例是存放在一个单独的返回栈中
这时如果我们在C页面按下Back返回键,那么是会直接回到A页面的,这是因为A、C两页面的实例是存放在同一个返回栈中的。当我们在A页面中再按下Back返回键时,会直接回到B页面,这是因为A、C页面的返回栈已经空了,于是就显示了另一个返回栈的栈顶,也就是B页面所在的返回栈,这时我们再按下Back返回键,这时所有返回栈都为空了,也就退出了程序(C页面——>A页面——>B页面——>Exit)。

二、Activity使用的小技巧

通过前面的学习你已经掌握了Activity的大部分基础内容了。为了方便你更好的在项目中使用和管理Activity,你还需要掌握几个小技巧。这些小技巧会在你的项目或者工作中起到很大的作用,在后续的实战文章(例如第13、16章)中也会有涉及到。

2.1 随时随地退出程序

当我们的项目越来越复杂,会有很多Activity,例如我们从A界面——>B界面——>C界面。这个时候,如果我们想退出应用程序是非常不方便的,我们需要连按多次Back键才行(按下Home键只是把程序挂起,并没有退出程序)。通过下面这个小技巧,就可以轻松实现注销或退出功能
我们新建一个名为ActivityController的单例类用来管理全局的Activity。通过ActivityController,不论在那个界面,只需要调用finishAllActivities()方法就可以关闭所有界面。ActivityController.kt的代码如下:
Kotlin Code:

/**
 * 用于管理全局Activity的单例类
 */
object ActivityController {

    //用于管理所有Activity的集合
    private val activities = ArrayList<Activity>()

    //往集合中添加Activity
    fun addActivity(targetActivity: Activity) {
        activities.add(targetActivity)
    }

    //从集合中移除Activity
    fun removeActivity(targetActivity: Activity) {
        activities.remove(targetActivity)
    }

    //关闭集合中的所有的Activity
    fun finishAllActivities() {
        for (activity in activities) {
            //判断Activity是否正在销毁中
            if (!activity.isFinishing) {
                activity.finish()
            }
        }
        //销毁所有Activity后清空集合
        activities.clear()
    }
}

Java Code:

/**
 * 用于管理全局Activity的单例类
 */
public class ActivityController {

    // 用于管理所有Activity的集合
    private static final List<Activity> activities = new ArrayList<>();

    // 私有构造函数,防止实例化
    private ActivityController() {
    }

    // 往集合中添加Activity
    public static void addActivity(Activity targetActivity) {
        activities.add(targetActivity);
    }

    // 从集合中移除Activity
    public static void removeActivity(Activity targetActivity) {
        activities.remove(targetActivity);
    }

    // 关闭集合中的所有的Activity
    public static void finishAllActivities() {
        for (Activity activity : activities) {
            // 判断Activity是否正在销毁中
            if (!activity.isFinishing()) {
                activity.finish();
            }
        }
        // 销毁所有Activity后清空集合
        activities.clear();
    }
}

我们将ActivityController声明为单例类,这样类中的所有方法都可以直接通过类名来访问。首先声明了一个ArrayList< Activity>集合用来存放所有的Activity,然后提供了addActivity()、removeActivity()、finishAllActivities()方法用来管理这个Activity集合。在finishAllActivities()方法中,我们遍历Activity集合中的每一个Activity,然后调用finish()方法将它们逐一销毁,最后调用clear()方法清空这个Activity集合。

在创建好单例工具类后,紧接着创建一个BaseActivity作为所有Activity的父类,这样便于我们对全局Activity进行管理。我们在BaseActivity的onCreate()方法和onDestroy()方法中实现了Activity集合元素的添加和移除。当某个Activity继承自BaseActivity后,在该Activity的生命周期中会自动调用父类BaseActivity的onCreate()和onDestroy()方法,进而实现Activity集合的添加和移除:

/**
 * 全局Activity的父类
 */
open class BaseActivity : AppCompatActivity() {

    override fun onCreate(savedInstanceState: Bundle?, persistentState: PersistableBundle?) {
        super.onCreate(savedInstanceState, persistentState)
        //将当前Activity添加到全局Activity列表中的
        ActivityController.addActivity(this)
    }

    override fun onDestroy() {
        super.onDestroy()
        //从全局Activity列表中移除当前Activity
        ActivityController.removeActivity(this)
    }
}

这样我们若想实现退出应用程序或者注销登陆操作,只需要调用ActivityController.finishAllActivities() 方法就可以了。

//销毁Activity列表中所有的Activity
ActivityController.finishAllActivities()

如果你想在销毁所有Activity后再杀掉当前应用程序的进程,以保证程序完全退出。你可以在调用finishAllActivities()方法后加上以下代码:

//kill当前应用程序进程
android.os.Process.killProcess(android.os.Process.myPid())

需要注意的是killProcess()方法只能用于杀掉当前程序的进程,不能用于杀掉其他程序(不然那就太离谱了)。

2.2 启动一个Activity的最佳写法

在实际项目或者工作中有很多Activity可能并不是你负责开发的,而你却要启动这个Activity。这个时候你很可能不知道要向该Activity中传递那些数据。我们可以通过下面这个方法轻松解决:

class NewsContentActivity : AppCompatActivity() {
	· · ·
	// 通过companion object定义一个静态方法
    companion object {
        fun actionStart(context: Context, title: String, content: String) {
            val intent = Intent(context, NewsContentActivity::class.java).apply {
                putExtra("news_title", title)
                putExtra("news_content", content)
            }
            context.startActivity(intent)
        }
    }

    override fun onCreate(savedInstanceState: Bundle?) {
    	· · ·
	}
	· · ·
}

假如我们需要跳转到NewsContentActivity,并且要求传入新闻的标题和新闻内容这两个数据。我们可以通过在companion object{ }结构声明一个静态方法actionStart(),然后通过intent将NewsContentActivity所需要的所有数据传递过来。这样我们如果想要在其他页面启动NewsContentActivity,可以使用actionStart()方法,Android Studio会要求我们传入所需要的数据才能启动NewsContentActivity。

NewsContentActivity.actionStart(parent.context,news.title,news.content)

效果如下图:
在这里插入图片描述

  • 4
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值