安卓四大组件-Activity

Activity

Activity是Android应用程序的基本组成部分之一。每一个Activity都是一个用户界面屏幕,通常包含一个或多个视图组件,如按钮、文本框等。Activity负责与用户的交互。

简单来说,Activity相当于一个应用程序的“页面”。在一个应用中,可以有多个Activity,并且它们之间可以进行跳转和传递数据。

创建Activity

1.使用Java创建一个MainActivity,继承自Activity或者Activity的子类AppCompatActivity

public class MainActivity extends AppCompatActivity {

    @Override

    protected void onCreate(Bundle savedInstanceState) {

        super.onCreate(savedInstanceState);

        setContentView(R.layout.activity_main);

    }

}

2.在 AndroidManifest.xml 中声明 Activity

<activity android:name=".MainActivity">

    <intent-filter>

        <action android:name="android.intent.action.MAIN" />

        <category android:name="android.intent.category.LAUNCHER" />

    </intent-filter>

</activity>

3.添加Activity关联的布局或者View视图

<?xml version="1.0" encoding="utf-8"?>

<androidx.constraintlayout.widget.ConstraintLayout 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:id="@+id/main"

    android:layout_width="match_parent"

    android:layout_height="match_parent"

    tools:context=".dialog.TestActivity">

<--在这个地方可以定义布局内的视图元素-->

</androidx.constraintlayout.widget.ConstraintLayout>

Activity的跳转

1.使用Intent跳转到SecondActivity,这种方式也被称为显式意图(明确指定Activity名称):

Intent intent = new Intent(this, SecondActivity.class);

startActivity(intent);

2.使用Intent,做隐式意图跳转Activity

//隐式意图跳转到Second

Intent intent = new Intent();

//添加隐式意图过滤器  指定事件和意图分类

intent.setAction("com.ls.acbjp.activity.SecondActivity");

intent.addCategory(Intent.CATEGORY_DEFAULT);

//intent.addCategory("android.intent.category.DEFAULT");

startActivity(intent);

隐式意图需要为目标Activity做如下配置,指定action以及category:

<activity android:name="com.ls.acbjp.activity.SecondActivity"

    android:exported="false"

    >

    <intent-filter>

        <action android:name="com.ls.acbjp.activity.SecondActivity"/>

        <category android:name="android.intent.category.DEFAULT"/>

    </intent-filter>

</activity>

Activity数据传递

往目标页面传值

1.Activity在跳转的时候可以借助Intent进行数据传递

//传递数据到Second页面

Intent intent = new Intent(this, SecondActivity.class);

//获取输入框里的数据

String string = etData.getText().toString();

if (string != null && string.length() > 0) {

    //传递给Second

    intent.putExtra("key_data", string);

    intent.putExtra("key_int", 666);

}

//传递一个实现了Serializable序列化接口的自定义类

//注意这种用法需要让User类实现Serializable接口

User user = new User("老孙", "31", 123456);

intent.putExtra("key_user", user);

//传递Parcelable序列化接口的自定义类

//注意这种用法需要让Student类实现Parcelable接口

Student student = new Student("老王,", "35", 6664);

intent.putExtra("key_stu",student);

//跳转页面

startActivity(intent);

2.在SecondActivity接收数据

//获取一个intent

Intent intent = getIntent();

//通过key接收main页面传递来的参数

String string = intent.getStringExtra("key_data");

Log.i(TAG, "onCreate: string = " + string);

//通过key_int接收main页面传递来的参数,如果没接收到,就让keyInt默认为0

int keyInt = intent.getIntExtra("key_int", 0);

//接收Serializable序列化接口的自定义类

User user = (User) intent.getSerializableExtra("key_user");

//接收实现了Parcelable序列化接口的自定义类

Student student = intent.getParcelableExtra("key_stu");

往上个页面回传值

1.想要往上个页面回传数据,就不能单纯的使用startActivity方法跳转页面,而是使用startActivityForResult:

//注意,这种方式被标记为已过时,在新的项目中,不建议用这种写法

//启动页面,并等待SecondActivity回传数据,设置请求码为9(请求码可以自定义)

startActivityForResult(new Intent(this, SecondActivity.class), 9);


 

/**

 * 重写Activity的onActivityResult方法,在这接受其他Activity页面返回的结果

 *

 * @param requestCode 跳转页面的时候指定的请求码

 * @param resultCode  其他页面回传的结果码

 * @param data        返回的意图(数据)

 */

@Override

protected void onActivityResult(int requestCode, int resultCode, @Nullable Intent data) {

    super.onActivityResult(requestCode, resultCode, data);

    //判断是哪里跳回到当前Activity的

    if (requestCode == 9 && resultCode == 8) {

        //通过key接收second页面返回的参数

        int keyCmd = data.getIntExtra("key_cmd", 0);

        String hello = data.getStringExtra("key_hello");

        Log.i(TAG, "onActivityResult: keyCmd = " + keyCmd);

        Log.i(TAG, "onActivityResult: hello = " + hello);

        etData.setText("接受到second的数据: " + keyCmd + hello);

    }

}

2.使用ActivityResultLauncher接受SecondActivity回传的数据,这是比startActivityForResult更新的方式,建议优先使用:

//声明activityResultLauncher,在这接受其他Activity页面返回的结果

private ActivityResultLauncher<Intent> activityResultLauncher = registerForActivityResult(

        new ActivityResultContracts.StartActivityForResult(),

        new ActivityResultCallback<ActivityResult>() {

            @Override

            public void onActivityResult(ActivityResult o) {

                int resultCode = o.getResultCode();

                Intent data = o.getData();

                if (resultCode == 8) {

                    //通过key接收second页面返回的参数

                    int keyCmd = data.getIntExtra("key_cmd", 0);

                    String hello = data.getStringExtra("key_hello");

                    Log.i(TAG, "onActivityResult: keyCmd = " + keyCmd);

                    Log.i(TAG, "onActivityResult: hello = " + hello);

                    etData.setText("接受到second的数据: " + keyCmd + hello);

                }

            }

        }

);


 

//跳转到second页面,并且等待second回传数据

activityResultLauncher.launch(new Intent(this, SecondActivity.class));

3.不管是在startActivityForResult,还是activityResultLauncher.launch,在SecondActivity都需要通过setResult往回传数据:

int cmd = 777;

String hello = "hello world!";

//设置了一个当前页面的处理结果,并且设置了一些回传的参数

Intent backIntent = new Intent();

backIntent.putExtra("key_cmd", cmd);

backIntent.putExtra("key_hello", hello);

//设置结果,并传1个结果码8,这里的结果码可以自定义

setResult(8, backIntent);

Activity生命周期

Activity在被创建到销毁的过程中,会有各种状态的切换,Activity类针对于这些状态,提供了对应的回调方法。通过这些回调方法,我们就能够知道Activity当前是什么状态。Activity 类提供了六个核心回调:onCreate()、onStart()、onResume()、onPause()、onStop() 和 onDestroy()。当 activity 进入新状态时,系统会调用其中每个回调。

1.系统首次创建 Activity 时触发onCreate() 方法。Activity会在创建后进入“已创建”状态;onCreate() 方法执行完毕后,activity 会进入“已开始”状态,系统会相继调用 onStart() 和 onResume() 方法;

2.当 activity 进入“已开始”状态时,系统会调用 onStart()。当应用准备 activity 进入前台并实现互动时,此调用会向用户显示该 activity。一旦此回调完成,activity 就会进入“已恢复”状态,系统会调用 onResume() 方法;

3.当 activity 进入“已恢复”状态时,会进入前台,然后系统会调用 onResume() 回调。这是应用与用户互动的状态。应用会保持这种状态,直到发生某些事件(例如设备收到来电、用户转到其他 activity 或设备屏幕关闭)时将焦点从应用移开。这些状态发生后,activity 会进入“已暂停”状态,系统会调用 onPause() 回调。

4.系统会将onPause()方法视为用户即将离开 activity 的第一个信号,但这并不总是意味着 activity 会被销毁,这个状态可以理解为“已暂停”。onPause() 方法的完成并不意味着 Activity 离开“已暂停”状态。相反,activity 会保持此状态,直到 activity 恢复或对用户完全不可见。如果 activity 恢复,系统会再次调用 onResume() 回调。

5.当您的 activity 对用户不再可见时,它会进入“已停止”状态,并且系统会调用 onStop() 回调。当新启动的 activity 覆盖整个屏幕时,可能会发生这种情况。此外,当 activity 完成运行并即将终止时,系统也会调用 onStop()。当 activity 进入“已停止”状态时,Activity 对象将驻留在内存中,它会保留所有状态和成员信息,但不会附加到窗口管理器。activity 恢复后会重新调用这些信息。进入“已停止”状态后,Activity 要么返回与用户互动,要么结束运行并消失。如果 Activity 返回,系统将调用 onRestart()。如果 Activity 完成运行,系统会调用 onDestroy()。

6.销毁 Ativity 之前,系统会先调用 onDestroy()。如果 activity 即将结束,则onDestroy() 是该 activity 收到的最后一个生命周期回调。如果因配置变更而调用 onDestroy(),系统会立即创建一个新的 activity 实例,然后在新配置中对该新实例调用 onCreate()。系统会出于以下两种原因之一调用此回调:

a.activity 即将结束,这是由于用户完全关闭 activity 或由于对 activity 调用了 finish()

b.由于配置变更(如设备旋转或进入多窗口模式),系统会暂时销毁 activity。

Activity配置变化Configuration Changes

Configuration Changes指的是设备的配置发生变化,比如屏幕方向、语言、键盘类型、屏幕大小、夜间模式等。这些变化会触发系统重新创建Activity,来适应新的配置。

当设备发生屏幕翻转时,系统默认会销毁当前的Activity并重新创建一个新的Activity实例。销毁时,系统会调用onPause()、onStop()、onDestroy()等生命周期方法,随后再次调用onCreate()来创建新的Activity。这个过程会导致数据的丢失,所以可以做以下处理:

1.在Manifest中声明处理配置变化:让Activity在屏幕翻转或者是屏幕大小发生变化的时候,不需要重建Activity,而是直接调用onConfigurationChanged方法,让我们在方法里处理变化带来的影响。

<activity android:name=".MainActivity"

          android:configChanges="orientation|screenSize">

</activity>

2.在Activity中重写onConfigurationChanged,手动处理

@Override

public void onConfigurationChanged(Configuration newConfig) {

    super.onConfigurationChanged(newConfig);

    if (newConfig.orientation == Configuration.ORIENTATION_LANDSCAPE) {

        // 处理横屏逻辑

    } else if (newConfig.orientation == Configuration.ORIENTATION_PORTRAIT) {

        // 处理竖屏逻辑

    }

    if (newConfig.screenWidthDp != Configuration.SCREEN_WIDTH_DP_UNDEFINED) {

        // 处理屏幕大小变化

    }    

}

指定Activity屏幕方向

在 AndroidManifest.xml 文件中为Activity 设置 android:screenOrientation 属性就可以限制屏幕方向:

- portrait:只允许竖屏模式。

- landscape:只允许横屏模式。

- sensor:根据设备的物理方向传感器来自动切换屏幕方向。

- user:根据用户的屏幕方向设置来确定屏幕方向。

- nosensor:忽略方向传感器并使用当前的显示方向。

保存和恢复Activity的界面状态

在某些场景下,系统有可能会销毁Activity中的一些数据,比如Activity长时间处于后台,并重新返回时。这时候可以使用一些方法对Activity中的数据做存储,回到前台的时候再恢复数据:

使用 onSaveInstanceState() 保存简单轻量的界面状态

注意:如果是大量数据,比如非常大的图片、视频,不建议这样存储。并且这个方法在用户主动关闭或者finish的情况下,不会被触发。

static final String STATE_SCORE = "playerScore";

static final String STATE_LEVEL = "playerLevel";

//在onSaveInstanceState中保存一些必要的数据

@Override

public void onSaveInstanceState(Bundle savedInstanceState) {

   

    savedInstanceState.putInt(STATE_SCORE, currentScore);

    savedInstanceState.putInt(STATE_LEVEL, currentLevel);

    // 必须调用父类方法,才可以达成保存状态的效果

    super.onSaveInstanceState(savedInstanceState);

}

恢复onSaveInstanceState中保存的数据

想要恢复onSaveInstanceState中的数据,可以从onCreate() 或者 onRestoreInstanceState() 回调方法入手,这两个方法都会接收一个Bundle对象;

//首次创建、屏幕翻转,Activity都需要触发onCreate方法

@Override

protected void onCreate(Bundle savedInstanceState) {

    super.onCreate(savedInstanceState);

    // 如果savedInstanceState不为空,表示这个Activity是之前就存在的实例对象,于是就可以从里面获取数据

    if (savedInstanceState != null) {

        //从保存状态里面恢复数据

        currentScore = savedInstanceState.getInt(STATE_SCORE);

        currentLevel = savedInstanceState.getInt(STATE_LEVEL);

    } else {

        //这里表示正常创建Activity,不需要处理数据恢复的情况

    }

}

//在onStart之后、onResume之前被调用,并且只在savedInstanceState不为null的时候被调用

@Override

protected void onRestoreInstanceState(Bundle savedInstanceState) {

    super.onRestoreInstanceState(savedInstanceState);    

    // 恢复EditText中的文本

    EditTexteditText = findViewById(R.id.edit_text);

    StringsavedText = savedInstanceState.getString("saved_text");    

    editText.setText(savedText);

}

二者如何选择

具体来说,它们的使用取决于你希望在哪个生命周期阶段恢复状态,以及如何处理不同的状态恢复逻辑。以下提供几点建议:

优先在 onCreate() 中使用 savedInstanceState 的情况通常包括以下几种:

1.状态恢复与初始化紧密相关:当你希望在Activity创建时立即恢复状态,并且这些状态的恢复与初始化逻辑密不可分。

2.简单的恢复需求:如果状态恢复逻辑相对简单,可以直接在 onCreate() 中完成,这样可以减少方法调用,简化代码结构。

什么时候用onRestoreInstanceState:

1.状态恢复逻辑复杂:如果状态恢复逻辑比较复杂,并且可能依赖其他生命周期方法,则应考虑将这些逻辑放在 onRestoreInstanceState() 中,以确保状态在正确的时间点恢复。

2.需要在UI可见后恢复状态:如果你需要确保UI已经完全加载(如布局已经设置完成)后再进行状态恢复,那么应该将恢复逻辑放在 onRestoreInstanceState() 中。

Activity任务栈

任务栈(Task Stack)是管理Activity启动和存储的关键概念。任务栈遵循后进先出原则,先启动的Activity会被放在栈底,后启动的Activity被放在栈顶。

任务栈的工作原理

1.启动一个Activity:当用户启动一个Activity时,系统会将它推入任务栈中,并将其置于栈顶。

2.返回上一个Activity:当用户按下返回按钮或调用 finish() 方法时,当前栈顶的Activity会被弹出并销毁,之前的Activity重新出现在栈顶并变得可见。

3.任务栈和多任务处理:每个应用通常有一个主任务栈(Main Task Stack),但在某些情况下,可以创建多个任务栈。例如,当应用中使用了 singleTask 或 singleInstance 启动模式时,系统可能会创建新的任务栈来处理不同的用户任务。

Activity启动模式

启动模式(Launch Mode)决定了当一个Activity启动时,它在系统中的行为方式。Android提供了四种主要的启动模式:

1.standard 启动模式

standard 是默认的启动模式。每次启动一个Activity时,都会创建该Activity的一个新实例并将其放入任务栈(Task Stack)中。

示例场景:

假设你有一个新闻应用,用户点击了文章列表中的一篇文章,应用会启动一个显示文章详情的Activity。如果用户点击了多篇不同的文章,系统将为每篇文章创建一个新的Activity实例。每次点击都会在任务栈中添加一个新的ArticleDetailActivity实例。

2. singleTop 启动模式

singleTop 模式与 standard 类似,但有一个关键区别:如果请求的Activity实例已经位于任务栈的顶部,则不会创建新的实例,而是重用当前顶部的实例。并且调用已有实例的onNewIntent()方法。

3.singleTask 启动模式

singleTask 模式确保在任务栈中只有一个该Activity的实例。如果该Activity已经存在于任务栈中,无论它在哪个位置,系统都会将其提到栈顶并清除它之上的其他Activity。如果MainActivity已经存在于任务栈中,系统会将它移到栈顶并销毁它之上的其他Activity。

示例场景:

假设你有一个银行应用,用户可以从多个地方进入主页面(比如从通知或其他Activity)。主页面应该是唯一的,无论用户从哪里进入,系统都不应该创建多个主页面实例。

4.singleInstance 启动模式

singleInstance 是最特殊的启动模式。它不仅确保在任务栈中只有一个该Activity的实例,还会将其放在一个独立的任务栈中。其他应用程序的Activity不会与这个Activity共享同一个任务栈。

示例场景:

假设你有一个视频播放应用,你希望视频播放页面总是在独立的任务栈中显示,以便用户可以在多个任务之间自由切换而不影响视频播放。VideoPlayerActivity会独立存在于一个任务栈中,其他Activity不会与其共享任务栈。

5.singleInstancePerTask 启动模式

这是Android 12中引入的新启动模式,专门为多任务、多窗口设计的场景。允许每个任务栈中存在一个该 Activity 的单实例,而不同的任务栈中可以有多个实例。可以理解为是SingleStance的升级版。

示例场景:

如果希望在不同任务栈中使用同一个Activity,但不共享同一个Activity(例如不同的任务、多窗口任务),singleInstancePerTask 是理想的选择。

Intent Flags

Intent Flags是在通过Intent启动Activity时设置的标志位,它们用于控制Activity的启动行为和任务栈的管理方式。

- FLAG_ACTIVITY_NEW_TASK

- 行为:如果目标Activity不存在于当前任务栈中,则创建一个新的任务栈,并将这个Activity作为栈的根Activity。

- 应用场景:通常用于从后台服务启动Activity或跨应用启动Activity的场景。

- FLAG_ACTIVITY_SINGLE_TOP

- 行为:如果目标Activity位于栈顶,则复用该实例,而不是创建新的实例。

- 应用场景:用于避免重复启动栈顶的Activity。

- FLAG_ACTIVITY_CLEAR_TOP

- 行为:如果目标Activity已经存在于任务栈中,并且不在栈顶,那么系统会销毁位于它上面的所有Activity,然后调用它的onNewIntent()方法。

- 应用场景:用于返回某个已存在的Activity并清除中间的Activity。

- FLAG_ACTIVITY_REORDER_TO_FRONT

- 行为:将栈中已有的目标Activity移动到栈顶,而不销毁它之上的其他Activity。

- 应用场景:当需要将一个以前启动过的Activity调到前台,而不改变其他Activity的顺序时使用。

Intent Flags使用方法

Intent intent = new Intent(this, LaunchMode1Activity.class);

intent.addFlags(Intent.FLAG_ACTIVITY_SINGLE_TOP);//使用addFlags指定标志位

startActivity(intent);

Intent Flags和启动模式有什么区别?

- 定义位置不同

- 启动模式是Activity在Manifest文件中定义的,它决定了Activity在任务栈中的基本行为模式。

- Intent Flags是在启动Activity时动态设置的,用于调整或覆盖启动模式的行为。

- 灵活性不同

- 启动模式是静态的,定义后通常不容易更改,除非在Manifest文件中修改。

- Intent Flags是动态的,可以在不同的场景中灵活使用来满足特定需求。

- 应用场景

- 启动模式适用于定义全局行为规则的Activity。

- Intent Flags适用于特定的Activity启动场景,可以与启动模式结合使用来实现更复杂的行为。

Fragment

Fragment是一个可以嵌入在Activity中的UI部分,它可以看作是Activity的子组件。一个Activity可以包含多个Fragment,每个Fragment有自己的生命周期、接收自己的输入事件,并且可以在运行时动态地添加或移除。

- fragment 定义和管理自己的布局,具有自己的生命周期,并且可以处理自己的输入事件

- fragment 不能独立存在。它们必须由 activity 或其他 fragment 托管。

静态添加Fragment

- 在Activity对应的xml布局文件中添加FrameLayout布局(建议优先使用FrameLayout的子类FragmentContainerView),并且通过name属性关联对应的Fragment。 注意添加id属性,否则会报错。

<androidx.fragment.app.FragmentContainerView

    android:id="@+id/my_fragment"

    android:name="com.ls.acbjp.activity.MyFragment"

    android:layout_width="match_parent"

    android:layout_height="wrap_content"

    app:layout_constraintStart_toStartOf="parent"

    app:layout_constraintTop_toTopOf="parent" />

动态添加Fragment

1.在xml中添加关联容器,这里不需要使用name声明关联的Fragment,而是在第二步的java代码中完成。

<androidx.fragment.app.FragmentContainerView

    android:id="@+id/fcv"

    android:layout_width="match_parent"

    android:layout_height="wrap_content"

    app:layout_constraintStart_toStartOf="parent"

    app:layout_constraintTop_toBottomOf="@id/btn_replace" />

2.通过Java代码对Fragment做管理

if (v.getId() == R.id.btn_add) {

    //添加

    MyFragment myFragment = new MyFragment();

    //在Activity中负责fragment的添加、删除、替换等操作

    FragmentManager fragmentManager = getSupportFragmentManager();

    //开始执行事务

    FragmentTransaction fragmentTransaction = fragmentManager.beginTransaction();

    //添加fragment  并且指定 关联的布局id、tag

    fragmentTransaction.add(R.id.fcv, myFragment, "tag_myFragment");

    fragmentTransaction.commit();//提交事务

} else if (v.getId() == R.id.btn_remove) {

    //移除

    FragmentManager fm = getSupportFragmentManager();

    FragmentTransaction ft = fm.beginTransaction();

    //通过tag查找到fragment

    Fragment tagMyFragment = fm.findFragmentByTag("tag_myFragment");

    //通过某个控件查找到fragment,会返回当前所关联的Fragment

    Fragment fragment = fm.findFragmentById(R.id.fcv);

    //移除Fragment

    ft.remove(tagMyFragment);

    ft.commit();

} else if (v.getId() == R.id.btn_replace) {

    //替换

    FragmentManager fm = getSupportFragmentManager();

    FragmentTransaction ft = fm.beginTransaction();

    ft.replace(R.id.fcv, new BlueFragment(), "tag_BlueFragment");

    ft.commit();

}

Fragment生命周期

尽管Fragment需要依赖于Activity,但每个 Fragment 实例都有自己的生命周期。 Fragment 会在添加、移除时以及进入或退出屏幕时完成其生命周期内各种状态的转换。

Fragment 类包含与 Fragment 生命周期中每项变化相对应的回调方法。其中包括 onCreate()、onStart()、onResume()、onPause()、onStop() 和 onDestroy()等。

此外还会有一些帮助我们更好的管理Fragment的UI状态以及资源而出现的回调,以下是Fragment完整的生命周期以及每个阶段的关键方法:

- onAttach(Context context)

- 说明: 当Fragment与Activity关联时调用,所以这个方法只会调用1次。此时可以获取Activity的引用(getActivity()方法)。

- onCreate(Bundle savedInstanceState)

- 说明: 当Fragment被创建时调用,用于初始化不依赖于视图的组件,例如获取资源或设置数据。所以不要在这里初始化UI元素。

- onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState)

- 说明: 为Fragment创建视图层次结构时调用。应在此方法中返回Fragment的根视图。

- onViewCreated(View view, Bundle savedInstanceState)

- 说明: 当视图创建完成后调用,可在此初始化UI组件或绑定数据。此时视图已经完全创建,可以访问UI元素。

- onActivityCreated(Bundle savedInstanceState)

- 说明: 当宿主Activity的onCreate()方法返回后调用,可在此与Activity交互或依赖于Activity的内容。从API 28开始已废弃,建议使用onViewCreated()就可以了。

- onStart()

- 说明: 当Fragment变为可见时调用,此时视图即将显示在屏幕上。

- onResume()

- 说明: 当Fragment可与用户交互时调用,Fragment处于活动状态。适合在此恢复暂停的操作或更新UI。

- onPause()

- 说明: 当Fragment失去用户焦点但仍然可见时调用,可以在此保存用户数据或暂停耗时操作。

- onStop()

- 说明: 当Fragment完全不可见时调用。适合在此保存状态或释放不再需要的资源。

- onDestroyView()

- 说明: 当与Fragment视图关联的资源被释放时调用。此时视图即将销毁,适合在此清理UI相关的资源。

- onDestroy()

- 说明: 当Fragment不再需要时调用,通常用于清理全局资源。适合在此释放持久数据,比如成员变量、某些图片资源或对象资源等等。

- onDetach()

- 说明: 当Fragment与Activity解除关联时调用。Fragment的生命周期结束,适合在此释放Activity引用。

- onSaveInstanceState()

- 在Fragment即将被销毁(例如,用户旋转屏幕或Fragment被放入后台)之前调用(通常在onPause()或onStop()之前调用)。它的作用是保存Fragment的瞬时状态(如UI状态或一些临时数据)到Bundle中,以便在恢复时使用。

- onViewStateRestored()

- 在onCreateView()之后、onStart()之前调用。它用于恢复先前保存的Fragment状态,与onSaveInstanceState()配对使用。

ViewPager2

使用ViewPager2可以配合Fragment实现左右滑动、上下滑动的切换效果;

1.旧版本项目,需要依赖ViewPager2或者androidx

 implementation 'com.google.android.material:material:1.10.0'

2.在布局中添加ViewPager2

<androidx.viewpager2.widget.ViewPager2  

    android:id="@+id/viewPager2"  

    android:layout_width="match_parent"  

    android:layout_height="wrap_content"  

    android:orientation="horizontal" />

3.ViewPager2设置适配器

        //准备一个存放Fragment的容器,与适配器关联

        ArrayList<Fragment> fragments = new ArrayList<>();

        RecommendFragment recommendFragment = new RecommendFragment();

        fragments.add(recommendFragment);

        fragments.add(new ShopFragment());

        fragments.add(new FollowFragment());

        fragments.add(new CityFragment());

        fragments.add(new ChoiceFragment());

        //为viewpager添加fragment适配器,

        viewPager.setAdapter(new FragmentStateAdapter(this) {

            @NonNull

            @Override

            public Fragment createFragment(int position) {

                //返回关联的fragment

                return fragments.get(position);

            }

            /**

             *

             * @return fragment个数

             */

            @Override

            public int getItemCount() {

                return fragments != null ? fragments.size() : 0;

            }

        });

4.ViewPager2的其他常见操作

        //指定viewpager滑动方向为竖直方向,默认是水平方向

        viewPager.setOrientation(ViewPager2.ORIENTATION_HORIZONTAL);

        //指定为false,表示不能主动滑动,true表示可以主动滑动

        viewPager.setUserInputEnabled(true);

        //添加滑动监听

        viewPager.registerOnPageChangeCallback(new ViewPager2.OnPageChangeCallback() {

            /**

             *  滑动的过程中会被调用

             */

            @Override

            public void onPageScrolled(int position, float positionOffset, int positionOffsetPixels) {

                super.onPageScrolled(position, positionOffset, positionOffsetPixels);

                Log.i(TAG, "onPageScrolled: position = " + position);

            }

            /**

             * 页面被选中时调用

             * @param position Position index of the new selected page.

             */

            @Override

            public void onPageSelected(int position) {

                super.onPageSelected(position);

                Log.i(TAG, "onPageSelected: position = " + position);

                //在页面滑动时让对应的RadioButton变成选中状态

                switch (position) {

                    case 0:

                        rbRecommend.setChecked(true);

                        break;

                    case 1:

                        rbShop.setChecked(true);

                        break;

                    case 2:

                        rbFollow.setChecked(true);

                        break;

                    case 3:

                        rbCity.setChecked(true);

                        break;

                    case 4:

                        rbChioce.setChecked(true);

                        break;

                }

            }

            /**

             * 滑动状态变化

             * @param state

             */

            @Override

            public void onPageScrollStateChanged(int state) {

                super.onPageScrollStateChanged(state);

                //state:空闲状态、拖动、缓动

//                ViewPager2.SCROLL_STATE_IDLE = 0

//                ViewPager2.SCROLL_STATE_DRAGGING = 1

//                ViewPager2.SCROLL_STATE_SETTLING = 2

                Log.i(TAG, "onPageSelected: state = " + state);

            }

        });

       

//RadioGroup与ViewPager2联动

RadioGroup radioGroup = findViewById(R.id.radio_group);

radioGroup.setOnCheckedChangeListener(new RadioGroup.OnCheckedChangeListener() {

    @Override

    public void onCheckedChanged(RadioGroup group, int checkedId) {

        //获取当前选中的RadioButton的id做判断

        if (checkedId == R.id.rb_recommend) {

            //通过代码指定viewpager显示哪一个position

            viewPager.setCurrentItem(0);

        } else if (checkedId == R.id.rb_shop) {

            viewPager.setCurrentItem(1);

        } else if (checkedId == R.id.rb_follow) {

            viewPager.setCurrentItem(2);

        } else if (checkedId == R.id.rb_city) {

            viewPager.setCurrentItem(3);

        } else if (checkedId == R.id.rb_chioce) {

            viewPager.setCurrentItem(4);

        }

    }

});

权限管理与声明

Android设备中存储了大量的用户个人数据,包括联系人、照片、位置等。这些数据对用户来说是非常敏感的,如果应用程序不加限制地访问这些数据,可能会造成用户隐私泄露。因此,权限管理可以确保只有用户明确授权的应用才能访问这些敏感信息。

不同Android版本的权限变化

- Android 6.0之前:安装时权限

- 在Android 6.0之前,权限是在应用安装时请求的。用户在安装应用时需要同意所有的权限请求,如果用户不同意,就无法安装应用。这种模型的缺点是用户往往不清楚这些权限的具体用途,也无法对单个权限进行选择。

- Android 6.0:运行时权限

- 从Android 6.0开始,引入了运行时权限的概念。应用需要在运行时动态请求某些敏感权限,如位置、摄像头、麦克风等。用户可以在运行时决定是否授予这些权限,这样用户可以更好地控制应用对敏感信息的访问。

- Android 8.0:后台位置权限

- Android 8.0引入了后台位置权限的限制,应用在后台运行时无法频繁获取位置信息,以此来保护用户隐私和设备电量。

- Android 10:位置权限的新选项

- Android 10增加了“仅在使用时允许”位置权限选项,用户可以选择只在应用程序前台时允许访问位置信息,从而进一步增强对隐私的控制。

- Android 11:一次性权限

- 在Android 11中,Google引入了一次性权限的概念,用户可以授予应用一次性访问某些敏感权限,如摄像头或麦克风。应用在每次访问这些权限时都需要重新请求授权。

- Android 12及以上:隐私仪表盘和麦克风/摄像头指示器

- Android 12中引入了隐私仪表盘,用户可以清楚地看到哪些应用访问了哪些权限。同时,系统还在状态栏中添加了麦克风和摄像头的使用指示器,当应用使用这些硬件时,用户会得到明确的提示。

- Android 13及以上:引入媒体权限

- 假如用户只是需要访问媒体文件(图片、视频、音频),那么只需要单独声明对应的权限就可以了,而不需要像之前那样,对整体的文件访问权限做声明。

权限声明时需要注意的规范

为了避免权限滥用、保障用户权益,建议在申请权限时考虑下面几个因素:

- 请求最少数量的权限:用多少,申请多少;

- 权限和操作相关联:不要一口气申请所有权限,而是在有需要用到某个权限的时候再声明;

- 考虑依赖库的权限:某些第三方库或者第三方服务,也会使用到权限,这时候需要注意他们不会滥用权限;

- 公开透明:必须明确告诉用户获取权限的原因。

权限的使用

1.在清单中添加声明

如果应用需要使用到一些权限,那么需要在清单中声明uses-permission表示当前APP包含这些权限:

<manifest ...>

    <uses-permission android:name="android.permission.CAMERA"/>

    <application ...>

        ...    

    </application>

</manifest>

2.在代码中请求权限,并处理结果

- 声明应用后不意味立刻能获取到权限,在6.0以上的系统还需要在运行时动态请求权限,此时应该先判断是否有权限:

//方法1:判断应用是否已经拥有某些权限

if (ContextCompat.checkSelfPermission(this, Manifest.permission.CAMERA)

    != PackageManager.PERMISSION_GRANTED) {

    // 权限尚未授予,在这里请求相关权限

    }else{

    //权限已经获取,可以正常执行相关操作            

    }

//方法2:用于判断是否应该向用户解释为什么需要某个权限

if (ActivityCompat.shouldShowRequestPermissionRationale(RPermissionActivity.this,

        android.Manifest.permission.CAMERA)) {

    //如果返回true,一般是因为用户已经拒绝过一次权限申请了,

    //在这个情况下,如果用户再次触发权限申请,可以在这里做一些获取权限的解释说明                    

}else{

    //如果返回false,说明不需要向用户解释为什么需要获取权限

    //这种情况一般是因为用户首次请求权限,或者是已经勾选了“不再询问”

}

- 如果权限尚未获取:

方法1:

//方法1: 调用Activity或者ActivityCompat中的requestPermissions方法

ActivityCompat.requestPermissions(this,new String[]{android.Manifest.permission.CAMERA},200);

//处理方法1请求方式返回的结果

@Override

public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {

    super.onRequestPermissionsResult(requestCode, permissions, grantResults);

    //判断请求码,从而确定是哪次请求

    if (requestCode == 200) {

        // 检查权限请求的结果

        if (grantResults.length > 0 && grantResults[0] == PackageManager.PERMISSION_GRANTED) {

            // 用户授予了权限,在这里可以直接操作打开相机

        } else {

            // 用户拒绝了权限

            if (!ActivityCompat.shouldShowRequestPermissionRationale(this, Manifest.permission.CAMERA)) {

                // 用户勾选了“不再询问”

                // 在此提示用户前往设置手动授予权限

                new AlertDialog.Builder(this)

                        .setTitle("需要相机权限")

                        .setMessage("应用需要相机权限来拍摄照片,请前往设置开启权限。")

                        .setPositiveButton("前往设置", (dialog, which) -> {

                            // 引导用户前往设置页面

                            Intent intent = new Intent(Settings.ACTION_APPLICATION_DETAILS_SETTINGS);

                            Uri uri = Uri.fromParts("package", getPackageName(), null);

                            intent.setData(uri);

                            startActivity(intent);

                        })

                        .setNegativeButton("取消", null)

                        .show();

            } else {

                // 用户拒绝了权限,但没有勾选“不再询问”

                Toast.makeText(this, "相机权限被拒绝", Toast.LENGTH_SHORT).show();

            }

        }

    }

}

方法2(推荐使用新方式):

//注册permission回调函数,用户处理完,会在这里返回结果

private ActivityResultLauncher<String> requestPermissionLauncher =

        registerForActivityResult(new ActivityResultContracts.RequestPermission(), isGranted -> {

            if (isGranted) {

                // 同意授权

                Toast.makeText(RPermissionActivity.this, "同意!", Toast.LENGTH_SHORT).show();

            } else {

                //拒绝授权

                if (!ActivityCompat.shouldShowRequestPermissionRationale(RPermissionActivity.this,

                        Manifest.permission.READ_EXTERNAL_STORAGE)) {

                    Toast.makeText(RPermissionActivity.this, "拒绝,且勾选了不询问", Toast.LENGTH_SHORT).show();

                } else {

                    Toast.makeText(RPermissionActivity.this, "拒绝,但没有勾选不再询问", Toast.LENGTH_SHORT).show();

                }

            }

        });

//使用requestPermissionLauncher请求权限

requestPermissionLauncher.launch(android.Manifest.permission.READ_EXTERNAL_STORAGE);

常见的权限

一般正常权限(安装时直接授予)

这些权限敏感度不高,在清单中声明即可

<uses-permission android:name="android.permission.INTERNET"/> //网络访问

<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE"/>//允许应用访问网络状态

<uses-permission android:name="android.permission.ACCESS_WIFI_STATE"/>//允许访问wifi

<uses-permission android:name="android.permission.WAKE_LOCK"/>//允许应用防止手机进入休眠状态

<uses-permission android:name="android.permission.VIBRATE"/>//允许应用手机震动

运行时权限

这些权限有一定敏感度,除了在清单中声明,还需要在代码中动态获取

ACCESS_FINE_LOCATION:访问精确的位置(使用 GPS 和网络提供的位置)

<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION"/>

ACCESS_COARSE_LOCATION:访问粗略的位置(使用网络提供的位置)

<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION"/>

READ_CONTACTS:读取联系人数据

<uses-permission android:name="android.permission.READ_CONTACTS"/>

WRITE_CONTACTS:写入联系人数据

<uses-permission android:name="android.permission.WRITE_CONTACTS"/>

READ_PHONE_STATE:读取电话状态(例如设备 ID、网络状态)

<uses-permission android:name="android.permission.READ_PHONE_STATE"/>

CALL_PHONE:直接拨打电话

<uses-permission android:name="android.permission.CALL_PHONE"/>

READ_CALL_LOG:读取通话记录

<uses-permission android:name="android.permission.READ_CALL_LOG"/>

WRITE_CALL_LOG:写入通话记录

<uses-permission android:name="android.permission.WRITE_CALL_LOG"/>

SEND_SMS:发送 SMS 短信

<uses-permission android:name="android.permission.SEND_SMS"/>

READ_SMS:读取 SMS 短信

<uses-permission android:name="android.permission.READ_SMS"/>

READ_EXTERNAL_STORAGE:读取外部存储的数据(在android13设备上,如果只是对媒体文件图片、视频、音频,那么可以只做单独的权限申请)

<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"/>

WRITE_EXTERNAL_STORAGE:写入外部存储的数据。

<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/>

访问图片文件

<uses-permission android:name="android.permission.READ_MEDIA_IMAGES"/>

访问视频文件

<uses-permission android:name="android.permission.READ_MEDIA_VIDEO"/>

访问音频文件

<uses-permission android:name="android.permission.READ_MEDIA_AUDIO"/>

CAMERA:访问设备的相机。

<uses-permission android:name="android.permission.CAMERA"/>

RECORD_AUDIO:录制音频。

<uses-permission android:name="android.permission.RECORD_AUDIO"/>

需要在设置页面手动授予的权限

这类权限涉及到比较高的系统权限或者是用户隐私,应用内部不能直接获取,所以需要跳转系统设置页:

//SYSTEM_ALERT_WINDOW:允许应用在其他应用的上层显示窗口。用户需要在设置中手动授予这个权限

<uses-permission android:name="android.permission.SYSTEM_ALERT_WINDOW"/>

Intentintent=newIntent(Settings.ACTION_MANAGE_OVERLAY_PERMISSION);

intent.setData(Uri.parse("package:" + getPackageName()));

startActivity(intent);

//REQUEST_INSTALL_PACKAGES:允许应用安装其他 APK 文件。用户需要在设置中手动授予这个权限。

<uses-permission android:name="android.permission.REQUEST_INSTALL_PACKAGES"/>

Intent intent = new Intent(Settings.ACTION_MANAGE_UNKNOWN_APP_SOURCES);

intent.setData(Uri.parse("package:" + getPackageName()));

startActivity(intent);

//WRITE_SETTINGS:允许应用修改系统设置。用户需要在设置中手动授予这个权限。

<uses-permission android:name="android.permission.WRITE_SETTINGS"/>

Intentintent=newIntent(Settings.ACTION_MANAGE_WRITE_SETTINGS);

intent.setData(Uri.parse("package:" + getPackageName()));

startActivity(intent);

//ACCESS_NOTIFICATION_POLICY:允许应用访问或修改通知策略。用户需要在设置中手动授予这个权限

<uses-permission android:name="android.permission.ACCESS_NOTIFICATION_POLICY"/>

Intent intent = new Intent(Settings.ACTION_NOTIFICATION_POLICY_ACCESS_SETTINGS);

startActivity(intent);

//MANAGE_EXTERNAL_STORAGE:允许应用访问所有外部存储文件。用户需要在设置中手动授予这个权限。

//安卓13(api版本33)开始,安卓引入“分区存储模式”以限制应用对存储文件的访问,当前这个权限几乎相当于自由读写所有文件

<uses-permission android:name="android.permission.MANAGE_EXTERNAL_STORAGE"/>

Intent intent = new Intent(Settings.ACTION_MANAGE_ALL_FILES_ACCESS_PERMISSION);

startActivity(intent);

更推荐使用ActivityResultLauncher:

/**

 * 是否拥有安装app的权限

 */

public boolean canInstallPermission() {

    boolean canInstall = true;//默认true表示拥有该权限

    //安卓8.0以上才能使用canRequestPackageInstalls方法判断是否有对应权限

    if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.O) {

        canInstall = getPackageManager().canRequestPackageInstalls();

    }

    return canInstall;

}

//注册页面跳转回调函数,用户处理完权限,会触发onActivityResult回调方法

private ActivityResultLauncher<Intent> activityResultLauncher = registerForActivityResult(

        new ActivityResultContracts.StartActivityForResult(),

        new ActivityResultCallback<ActivityResult>() {

            @RequiresApi(api = Build.VERSION_CODES.O)

            @Override

            public void onActivityResult(ActivityResult result) {

                if (result.getResultCode() == Activity.RESULT_CANCELED) {

                    //用户取消操作

                    Toast.makeText(SettingsPremissionActivity.this, "权限授予失败", Toast.LENGTH_SHORT).show();

                } else if (result.getResultCode() == Activity.RESULT_OK) {

                    // 用户已授予安装未知来源应用的权限

                    //再次调用权限判断是否真的已经授权

                    boolean canInstall = canInstallPermission();

                    if (canInstall) {

                        // 处理允许安装未知来源应用的逻辑

                        refreshTextView();

                        Toast.makeText(SettingsPremissionActivity.this, "已获取权限", Toast.LENGTH_SHORT).show();

                    }

                }

            }

        }

);

//跳转到系统设置页面

Intent intent = new Intent(Settings.ACTION_MANAGE_UNKNOWN_APP_SOURCES);

intent.setData(Uri.parse("package:" + getPackageName()));

activityResultLauncher.launch(intent);

硬件声明(扩展)

某些权限可能与硬件有关,如果想要在没有该设备的设备上运行APP,需要做如下声明。但如果没有在 <uses-feature> 中将 android:required 设置为 false 声明,Android 会假定应用必须在有该硬件的情况下才能运行。 随后,系统会阻止某些设备安装您的 app:

<uses-feature

    android:name="android.hardware.camera"

    android:required="false" />//false表示如果设备没有相机硬件,但APP仍然可以安装并运行,只是不能正常使用相机功能

如果需要对硬件做可用性声明,Java代码中也需要做处理:

// 检查你的应用是否运行在有前置摄像头的设备上

if (getApplicationContext().getPackageManager().hasSystemFeature(

        PackageManager.FEATURE_CAMERA_FRONT)) {

    // 有摄像头,可以继续正常执行代码

} else {

    // 没有摄像头,不要调用相机相关代码,做个提示就行了

    Toast.makeText(RPermissionActivity.this, "你没相机,用不了!", Toast.LENGTH_SHORT).show();

}

注意:从常见的商用开发场景来看,这些情况我们只要稍做了解就可以,一般不需要考虑。因为开放的APP肯定都会搭载具有丰富硬件的设备上。而如果是专门为某些设备定制的APP,就都是定向安装,这时候才需要开发者来注意这些事情。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值