Android基础知识

Android 基础知识一

简介

具体参考Android官方简介

应用组件

利用 Android 应用框架,您可以使用一组可重复使用的组件创建丰富的新应用。此部分阐述您可以如何构建用于定义应用构建基块的组件,以及如何使用 Intent 将这些组件连接在一起。

Intent和Intent过滤器

Intent是一种消息传递对象,可以使用它从其它应用组件请求操作,可以通过多种方式促进组件之间通信,主要的是以下三中:

  • 启动Activity

    通过startActivity(intent)来启动activity 。如果需要返回值,则可以调用startActivityForResult()

  • 启动Service

    通过startService(intent),如果服务旨在使用客户端-服务器接口, bindService(intent, mConnection, Context.BIND_AUTO_CREATE);

  • 传递Broadcasts

    通过给sendBroadcast(intent)启动广播

Intent类型

Intent分为两种类型,如下:

  • 显示intent

    按名称(完全限定类名)指定要启动的组件。 通常,您会在自己的应用中使用显式 Intent 来启动组件,这是因为您知道要启动的 Activity 或服务的类名。例如,启动新 Activity 以响应用户操作,或者启动服务以在后台下载文件。

    举例:

    第一个Activity启动

    // 显示启动
    Intent intent = new Intent(MainActivity.this,SecondActivity.class);
    intent.putExtra("name","第二个Activity");
    startActivity(intent);
    

    第二个Activity接收处理:

    Intent intent = getIntent();
    String name = intent.getStringExtra("name");
    
  • 隐式intent

    // 隐式启动
    // Create the text message with a string
    Intent sendIntent = new Intent();
    sendIntent.setAction(Intent.ACTION_SEND);
    sendIntent.putExtra(Intent.EXTRA_TEXT, "for test");
    sendIntent.setType("text/plain");
    
    // Verify that the intent will resolve to an activity
    if (sendIntent.resolveActivity(getPackageManager()) !=null) {
            startActivity(sendIntent);
    }
    
    Intent i = new Intent();
    // 其中action需要在AndroidManifest.xml里面注册
    i.setAction("com.seconddemo.otheractivity");
    i.putExtra("test","keke");
    startActivity(i);
    

注意:用户可能没有任何应用处理您发送到 startActivity() 的隐式 Intent。如果出现这种情况,则调用将会失败,且应用会崩溃。要验证 Activity 是否会接收 Intent,请对 Intent 对象调用 resolveActivity(): (intent.resolveActivity(context.getPackageManager())。如果结果为非空,则至少有一个应用能够处理该 Intent,且可以安全调用 startActivity()。也可以启动时增加 如果结果为空,则不应使用该 Intent。如有可能,您应停用发出该 Intent 的功能。当然也可以自定义启动方法并捕获ActivityNotFoundException来规避,示例如下:

 private void startExternalActivity(final Context context, final Intent intent) {
        try {
            context.startActivity(intent);
        } catch (final ActivityNotFoundException ex) {
            android.util.Log.w(LogUtil.BUGLE_TAG, "Couldn't find activity:", ex);
            Toast.makeText(getApplicationContext(),"找不到合适的应用",Toast.LENGTH_LONG);
        }
    }

注意:为了确保应用的安全性,启动 Service 时,请始终使用显式 Intent,且不要为服务声明 Intent 过滤器。使用隐式 Intent 启动服务存在安全隐患,因为您无法确定哪些服务将响应 Intent,且用户无法看到哪些服务已启动。从 Android 5.0(API 级别 21)开始,如果使用隐式 Intent 调用 bindService(),系统会引发异常。

强制使用应用的选择器

要显示选择器,可使用createChooser()创建Intent,并将其传递给StartActivity()

Intent sendIntent = new Intent();
sendIntent.setAction(Intent.ACTION_SEND);
// 设置类型
sendIntent.setType("text/plain");
sendIntent.putExtra(Intent.EXTRA_TEXT, text);
Intent chooser = Intent.createChooser(sendIntent, "share");
// Verify the original intent will resolve to at least one activity
if (sendIntent.resolveActivity(getPackageManager()) != null) {
    startActivity(chooser);
}

注意:
if (sendIntent.resolveActivity(getPackageManager()) != null) {
startActivity(chooser);
}

接受隐式Intent

要使应用可以接受哪些隐式intent,需要在AndroidManifest.xml文件里面使用元素为每一个应用组件申请一个或者多个intent过滤器。每个 Intent 过滤器均根据 Intent 的操作、数据和类别指定自身接受的 Intent 类型。

注:显示Intent始终会传递给其他目标,无论组件声明的Intent过滤器如果均是如此

一般的我们按照下面添加

<activity android:name=".OtherActivity">
    <intent-filter>
        <action android:name="com.seconddemo.otheractivity"/>
        <category android:name="android.intent.category.DEFAULT"/>
    </intent-filter>
</activity>

可以创建一个包括多个 、 或 实例的过滤器。创建时,仅需确定组件能够处理这些过滤器元素的任何及所有组合即可。

注意:为了避免无意中运行不同应用的 Service,请始终使用显式 Intent 启动您自己的服务,且不必为该服务声明 Intent 过滤器。

对于所有 Activity,您必须在清单文件中声明 Intent 过滤器。但是,广播接收器的过滤器可以通过调用 registerReceiver() 动态注册。 稍后,您可以使用 unregisterReceiver() 注销该接收器。这样一来,应用便可仅在应用运行时的某一指定时间段内侦听特定的广播。

过滤器实例

为了更好地了解一些 Intent 过滤器的行为,我们一起来看看从社交共享应用的清单文件中截取的以下片段。

<activity android:name="ShareActivity">
    <!-- This activity handles "SEND" actions with text data -->
    <intent-filter>
        <action android:name="android.intent.action.SEND"/>
        <category android:name="android.intent.category.DEFAULT"/>
        <data android:mimeType="text/plain"/>
    </intent-filter>
    <!-- This activity also handles "SEND" and "SEND_MULTIPLE" with media data -->
    <intent-filter>
        <action android:name="android.intent.action.SEND"/>
        <action android:name="android.intent.action.SEND_MULTIPLE"/>
        <category android:name="android.intent.category.DEFAULT"/>
        <data android:mimeType="application/vnd.google.panorama360+jpg"/>
        <data android:mimeType="image/*"/>
        <data android:mimeType="video/*"/>
    </intent-filter>
</activity>
通用Intent

这个就不多做说明了,主要是几种可用于执行的常见操作的隐式Intent,参考通用Intennt

下面就举一个联系人例子

static final int REQUEST_SELECT_CONTACT = 1;

public void selectContact() {
    Intent intent = new Intent(Intent.ACTION_PICK);
    // 通过CibtactsContract.Contacts.CONTENT_TYPE去访问联系人信息
    intent.setType(ContactsContract.Contacts.CONTENT_TYPE);
    if (intent.resolveActivity(getPackageManager()) != null) {
        startActivityForResult(intent, REQUEST_SELECT_CONTACT);
    }
}

@Override
protected void onActivityResult(int requestCode, int resultCode, Intent data) {
    if (requestCode == REQUEST_SELECT_CONTACT && resultCode == RESULT_OK) {
        // 可以从这个data里面获取联系人的数据
        Uri contactUri = data.getData();
        // Do something with the selected contact at contactUri
        ...
    }
}

Activity

Activity 是一个应用组件,用户可与其提供的屏幕进行交互,以执行拨打电话、拍摄照片、发送电子邮件或查看地图等操作。 每个 Activity 都会获得一个用于绘制其用户界面的窗口。窗口通常会充满屏幕,但也可小于屏幕并浮动在其他窗口之上。

Ativity
创建Activity

需要继承Activity或者是继承Activiy子类,然后在其中实现Activity的生命周期各种状态转变时系统调用的回调方法。

  • onCreate()

您必须实现此方法。系统会在创建您的 Activity 时调用此方法。您应该在实现内初始化 Activity 的必需组件。 最重要的是,您必须在此方法内调用 setContentView(),以定义 Activity 用户界面的布局。

  • onPause()

系统将此方法作为用户离开 Activity 的第一个信号(但并不总是意味着 Activity 会被销毁)进行调用。 您通常应该在此方法内确认在当前用户会话结束后仍然有效的任何更改(因为用户可能不会返回)。

实现用户界面

在onCreate()方法里面使用setContentView(R.layout.activity_main).也可以在Activity里面创建新的View,然后将新的View插入ViewGroup来创建视图层次。

在清单文件中声明Activity
<activity   android:name=".MainActivity"
            android:label="@string/launcherActivityLabel"
            android:theme="@style/DialtactsActivityTheme"
            android:launchMode="singleTask"
            android:clearTaskOnLaunch="true"
            android:icon="@mipmap/ic_launcher_phone"
            android:windowSoftInputMode="stateAlwaysHidden|adjustNothing"
            android:resizeableActivity="true">
    <intent-filter>
        <action android:name="android.intent.action.MAIN" />
        <category android:name="android.intent.category.LAUNCHER" />
    </intent-filter>
</activity>

可以在使用Intentn过滤器,
元素指定这个是程序的“主”入口,元素指定此 Activity 应列入系统的应用启动器内(以便用户启动该 Activity)。

启动Activity

调用StartActivity()启动Activity,下面例子是传值并且启动Activity

Intent intent = new Intent(this, SecondClass.this);
intent.putExtra("name","qin");
startActivity(intent);
startActivityForResult()

举例说明:

//启动SecondActivity
Intent intent = new Intent(MainActivity.this,SecondActivity.class);
intent.putExtra("name","第二个Activity");
startActivityForResult(intent,REQUEST_CODE);

@Override
protected void onActivityResult(int requestCode, int resultCode, Intent data) {
    // resultCode判断请求是否成功,requestCode与上面startActivityForResult发送的第二个参数匹配
    if (resultCode == 2 && requestCode == REQUEST_CODE) {
        Log.v(TAG, data.getStringExtra("SecondName"));
    }
    super.onActivityResult(requestCode, resultCode, data);
    }

SecondActivity调用setResult()方法

// SecondActiviy里面setResult
getIntent().putExtra("SecondName","第二个Activity返回");
setResult(2,intent);
结束Activity

调用finish()方法去结束当前Activity

Activity生命周期

Activity一般以以下三种状态存在:

  • OnResume()

    此Activity位于屏幕前台,并且具有用户焦点

  • onPause()

    另一个Activity位于屏幕前台并具有用户焦点,但此Activity仍可见。也就是说,另一个Activity显示在此Activity上方,并且该Activity部分透明或未覆盖整个屏幕。暂停的Activity处于完全活动状态(Activity对象保留在内存中,它保留了所有状态和成员信息,并与窗口管理器保持连接),但是在内存极度不足的情况下,会被系统终止。

  • onStop()

    该Activity被另一个Activity完全遮盖(该Activity目前位于“后台”)。已停止的Activity同样仍处于活动状态(Activity对象保留在内存中,它保留了所有状态和成员信息,但未与窗口管理器连接)。不过,它对用户不再可见,在其他需要内存是可能会被系统终止。

下图说明了Activity再状态变化期间可能经过的路径,矩形表示回调方法,当Activity在不同的状态之间转换,可以实现这些方法来执行操作
image

详细的Activity生命周期参考官方文档Activity生命周期

onSaveInstanceState()

管理 Activity 生命周期的引言部分简要提及,当 Activity 暂停或停止时,Activity 的状态会得到保留。 确实如此,因为当 Activity 暂停或停止时,Activity 对象仍保留在内存中 — 有关其成员和当前状态的所有信息仍处于活动状态。 因此,用户在 Activity 内所做的任何更改都会得到保留,这样一来,当 Activity 返回前台(当它“继续”)时,这些更改仍然存在。

不过,当系统为了恢复内存而销毁某项 Activity 时,Activity 对象也会被销毁,因此系统在继续 Activity 时根本无法让其状态保持完好,而是必须在用户返回 Activity 时重建 Activity 对象。但用户并不知道系统销毁 Activity 后又对其进行了重建,因此他们很可能认为 Activity 状态毫无变化。 在这种情况下,您可以实现另一个回调方法对有关 Activity 状态的信息进行保存,以确保有关 Activity 状态的重要信息得到保留:onSaveInstanceState()。

系统会先调用 onSaveInstanceState(),然后再使 Activity 变得易于销毁。系统会向该方法传递一个 Bundle,您可以在其中使用 putString() 和 putInt() 等方法以名称-值对形式保存有关 Activity 状态的信息。然后,如果系统终止您的应用进程,并且用户返回您的 Activity,则系统会重建该 Activity,并将 Bundle 同时传递给 onCreate() 和 onRestoreInstanceState()。您可以使用上述任一方法从 Bundle 提取您保存的状态并恢复该 Activity 状态。如果没有状态信息需要恢复,则传递给您的 Bundle 是空值(如果是首次创建该 Activity,就会出现这种情况)。

   @Override
    protected void onSaveInstanceState(Bundle outState) {
        super.onSaveInstanceState(outState);
        outState.putString(KEY_SEARCH_QUERY, mSearchQuery);
        outState.putBoolean(KEY_IN_REGULAR_SEARCH_UI, mInRegularSearch);
        outState.putBoolean(KEY_IN_DIALPAD_SEARCH_UI, mInDialpadSearch);
        outState.putBoolean(KEY_FIRST_LAUNCH, mFirstLaunch);
        outState.putBoolean(KEY_IS_DIALPAD_SHOWN, mIsDialpadShown);
        /// M: Save and restore the mPendingSearchViewQuery
        outState.putString(KEY_PENDING_SEARCH_QUERY, mPendingSearchViewQuery);
        mActionBarController.saveInstanceState(outState);

        mIsDialpadShown = false;
        if (mListsFragment != null) {
            mIsDialpadShown = mListsFragment.isDialpadShow();
        }
        outState.putBoolean(KEY_IS_DIALPAD_SHOWN, mIsDialpadShown);
        mPendingSearchViewQuery = null;
        if (mListsFragment != null) {
            mPendingSearchViewQuery = mListsFragment.getDialpadQuery();
        }
        outState.putString(KEY_PENDING_SEARCH_QUERY, mPendingSearchViewQuery);

        mStateSaved = true;
    }

image

注:无法保证系统会在销毁您的 Activity 前调用 onSaveInstanceState(),因为存在不需要保存状态的情况(例如用户使用“返回”按钮离开您的 Activity 时,因为用户的行为是在显式关闭 Activity)。 如果系统调用 onSaveInstanceState(),它会在调用 onStop() 之前,并且可能会在调用 onPause() 之前进行调用。

不过,即使您什么都不做,也不实现 onSaveInstanceState(),Activity 类的 onSaveInstanceState() 默认实现也会恢复部分 Activity 状态。具体地讲,默认实现会为布局中的每个 View 调用相应的 onSaveInstanceState() 方法,让每个视图都能提供有关自身的应保存信息。Android 框架中几乎每个小部件都会根据需要实现此方法,以便在重建 Activity 时自动保存和恢复对 UI 所做的任何可见更改。例如,EditText 小部件保存用户输入的任何文本,CheckBox 小部件保存复选框的选中或未选中状态。您只需为想要保存其状态的每个小部件提供一个唯一的 ID(通过 android:id 属性)。如果小部件没有 ID,则系统无法保存其状态。

Configuration Changes

有些设备配置可能会在运行时发生变化(例如屏幕方向、键盘可用性及语言)。 发生这种变化时,Android 会重启正在运行的 Activity(先后调用 onDestroy() 和 onCreate())。重启行为旨在通过利用与新设备配置匹配的备用资源自动重新加载您的应用,来帮助它适应新配置。

但是,可能会遇到这种情况:重启应用并恢复大量数据不仅成本高昂,而且给用户留下糟糕的使用体验。 在这种情况下,有两个其他选择:

a.在配置变更期间保留对象

允许 Activity 在配置变更时重启,但是要将有状态对象传递给 Activity 的新实例。

b.自行处理配置变更

阻止系统在某些配置变更期间重启 Activity,但要在配置确实发生变化时接收回调,这样,您就能够根据需要手动更新 Activity。

处理变更期间保留对象

如果重启 Activity 需要恢复大量数据、重新建立网络连接或执行其他密集操作,那么因配置变更而引起的完全重启可能会给用户留下应用运行缓慢的体验。 此外,依靠系统通过onSaveInstanceState() 回调为您保存的 Bundle,可能无法完全恢复 Activity 状态,因为它并非设计用于携带大型对象(例如位图),而且其中的数据必须先序列化,再进行反序列化,这可能会消耗大量内存并使得配置变更速度缓慢。 在这种情况下,如果 Activity 因配置变更而重启,则可通过保留 Fragment 来减轻重新初始化 Activity 的负担。此片段可能包含对您要保留的有状态对象的引用。

要在运行时配置变更期间将有状态的对象保留在片段中,请执行以下操作:

  • 扩展 Fragment 类并声明对有状态对象的引用。
  • 在创建片段后调用 setRetainInstance(boolean)。
  • 将片段添加到 Activity。
  • 重启 Activity 后,使用 FragmentManager 检索片段。

例如,按如下方式定义片段:

public class RetainedFragment extends Fragment {

    // data object we want to retain
    private MyDataObject data;

    // this method is only called once for this fragment
    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        // retain this fragment
        setRetainInstance(true);
    }

    public void setData(MyDataObject data) {
        this.data = data;
    }

    public MyDataObject getData() {
        return data;
    }
}

注意:尽管您可以存储任何对象,但是切勿传递与 Activity 绑定的对象,例如,Drawable、Adapter、View 或其他任何与 Context 关联的对象。否则,它将泄漏原始 Activity 实例的所有视图和资源。 (泄漏资源意味着应用将继续持有这些资源,但是无法对其进行垃圾回收,因此可能会丢失大量内存。)

然后,使用 FragmentManager 将片段添加到 Activity。在运行时配置变更期间再次启动 Activity 时,您可以获得片段中的数据对象。 例如,按如下方式定义 Activity:

public class MyActivity extends Activity {

    private RetainedFragment dataFragment;

    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.main);

        // find the retained fragment on activity restarts
        FragmentManager fm = getFragmentManager();
        dataFragment = (DataFragment) fm.findFragmentByTag(“data”);

        // create the fragment and data the first time
        if (dataFragment == null) {
            // add the fragment
            dataFragment = new DataFragment();
            fm.beginTransaction().add(dataFragment, “data”).commit();
            // load the data from the web
            dataFragment.setData(loadMyData());
        }

        // the data is available in dataFragment.getData()
        ...
    }

    @Override
    public void onDestroy() {
        super.onDestroy();
        // store the data in the fragment
        dataFragment.setData(collectMyLoadedData());
    }
}

在此示例中,onCreate() 添加了一个片段或恢复了对它的引用。此外,onCreate() 还将有状态的对象存储在片段实例内部。onDestroy() 对所保留的片段实例内的有状态对象进行更新。

自行处理配置

如果应用在特定配置变更期间无需更新资源,并且因性能限制您需要尽量避免重启,则可声明 Activity 将自行处理配置变更,这样可以阻止系统重启 Activity。

注:自行处理配置变更可能导致备用资源的使用更为困难,因为系统不会为您自动应用这些资源。 只能在您必须避免 Activity 因配置变更而重启这一万般无奈的情况下,才考虑采用自行处理配置变更这种方法,而且对于大多数应用并不建议使用此方法。

例如,以下清单文件代码声明的 Activity 可同时处理屏幕方向变更和键盘可用性变更:

        <activity android:name="com.android.incallui.InCallActivity"
                  android:theme="@style/Theme.InCallScreen"
                  android:label="@string/phoneAppLabel"
                  android:excludeFromRecents="true"
                  android:launchMode="singleInstance"
                  android:configChanges="screenSize|smallestScreenSize|screenLayout|orientation|keyboardHidden|keyboard|navigation|mnc|mcc"
                  android:exported="false"
                  android:screenOrientation="nosensor"
                  android:directBootAware="true"
                  android:resizeableActivity="@bool/enable_multi_window">
        </activity>

现在,当其中一个配置发生变化时,MyActivity 不会重启。相反,MyActivity 会收到对 onConfigurationChanged() 的调用。向此方法传递 Configuration 对象指定新设备配置。您可以通过读取 Configuration 中的字段,确定新配置,然后通过更新界面中使用的资源进行适当的更改。调用此方法时,Activity 的 Resources 对象会相应地进行更新,以根据新配置返回资源,这样,您就能够在系统不重启 Activity 的情况下轻松重置 UI 的元素。

注意:从 Android 3.2(API 级别 13)开始,当设备在纵向和横向之间切换时,“屏幕尺寸”也会发生变化。因此,在开发针对 API 级别 13 或更高版本(正如 minSdkVersion 和 targetSdkVersion 属性中所声明)的应用时,若要避免由于设备方向改变而导致运行时重启,则除了 “orientation” 值以外,您还必须添加 “screenSize” 值。 也就是说,您必须声明 android:configChanges=“orientation|screenSize”。但是,如果您的应用面向 API 级别 12 或更低版本,则 Activity 始终会自行处理此配置变更(即便是在 Android 3.2 或更高版本的设备上运行,此配置变更也不会重启 Activity)。

        <activity android:name=".MainActivity"
            android:configChanges="orientation|screenSize">
            <intent-filter>
                <action android:name="android.intent.action.MAIN" />
                <category android:name="android.intent.category.LAUNCHER" />
            </intent-filter>
        </activity>

以下 onConfigurationChanged() 实现检查当前设备方向:

@Override
public void onConfigurationChanged(Configuration newConfig) {
    super.onConfigurationChanged(newConfig);

    // Checks the orientation of the screen
    if (newConfig.orientation == Configuration.ORIENTATION_LANDSCAPE) {
        Toast.makeText(this, "landscape", Toast.LENGTH_SHORT).show();
    } else if (newConfig.orientation == Configuration.ORIENTATION_PORTRAIT){
        Toast.makeText(this, "portrait", Toast.LENGTH_SHORT).show();
    }
}

不加android:configChanges横竖屏切换生命周期

//第一次进入
12-26 21:27:58.957 18323-18323/com.configurationchangesdemo D/liqiang: onCreate:
12-26 21:27:58.964 18323-18323/com.configurationchangesdemo D/liqiang: onStart:
12-26 21:27:58.968 18323-18323/com.configurationchangesdemo D/liqiang: onResume:
//切换成横屏
12-26 21:34:53.794 18323-18323/com.configurationchangesdemo D/liqiang: onPause:
12-26 21:34:53.796 18323-18323/com.configurationchangesdemo D/liqiang: onSaveInstanceState: name = jack
12-26 21:34:53.801 18323-18323/com.configurationchangesdemo D/liqiang: onStop:
12-26 21:34:53.802 18323-18323/com.configurationchangesdemo D/liqiang: onDestroy:
12-26 21:34:53.930 18323-18323/com.configurationchangesdemo D/liqiang: onCreate:
12-26 21:34:53.932 18323-18323/com.configurationchangesdemo D/liqiang: onStart:
12-26 21:34:53.933 18323-18323/com.configurationchangesdemo D/liqiang: onRestoreInstanceState: name = jack
12-26 21:34:53.935 18323-18323/com.configurationchangesdemo D/liqiang: onResume:
//切换成竖屏
12-26 21:35:44.087 18323-18323/com.configurationchangesdemo D/liqiang: onPause:
12-26 21:35:44.088 18323-18323/com.configurationchangesdemo D/liqiang: onSaveInstanceState: name = jack
12-26 21:35:44.094 18323-18323/com.configurationchangesdemo D/liqiang: onStop:
12-26 21:35:44.094 18323-18323/com.configurationchangesdemo D/liqiang: onDestroy:
12-26 21:35:44.157 18323-18323/com.configurationchangesdemo D/liqiang: onCreate:
12-26 21:35:44.160 18323-18323/com.configurationchangesdemo D/liqiang: onStart:
12-26 21:35:44.160 18323-18323/com.configurationchangesdemo D/liqiang: onRestoreInstanceState: name = jack
12-26 21:35:44.166 18323-18323/com.configurationchangesdemo D/liqiang: onResume:

添加android:configChanges="orientation|screenSize"横竖屏切换生命周期

//第一次进入
12-26 21:36:49.237 18534-18534/? D/liqiang: onCreate:
12-26 21:36:49.238 18534-18534/? D/liqiang: onStart:
12-26 21:36:49.241 18534-18534/? D/liqiang: onResume:
//切换成横屏
12-26 21:41:38.632 18534-18534/com.configurationchangesdemo D/liqiang: onConfigurationChanged:
//切换成竖屏
12-26 21:42:10.048 18534-18534/com.configurationchangesdemo D/liqiang: onConfigurationChanged:

如果需要在Activity里面处理哪些配置变更的详细信息,则可以查询android:configChanges


Fragment
Fragment 原理

参考Fragment设计原理

如何创建Fragment

要想创建片段,必须创建Fragment子类(或者已有的子类),通常我们需要是想Fragment以下的生命周期:

  • onCreate()

    系统会在创建Fragment的时候调用这个方法,

  • onCreateView()

    系统会在片段首次绘制其用户界面时调用此方法,要想为您的片段绘制 UI,您从此方法中返回的 View必须是片段布局的根视图。如果片段未提供 UI,您可以返回 null。

  • onPause()

    系统将此方法作为用户离开片段的第一个信号(但并不总是意味着此片段会被销毁)进行调用。 您通常应该在此方法内确认在当前用户会话结束后仍然有效的任何更改(因为用户可能不会返回)

下面是几个扩展的子类:

  • DialogFragment

    显示浮动对话框。使用此类创建对话框可有效地替代使用 Activity 类中的对话框帮助程序方法,因为您可以将片段对话框纳入由 Activity 管理的片段返回栈,从而使用户能够返回清除的片段。

  • ListFragment

    显示由适配器(如 SimpleCursorAdapter)管理的一系列项目,类似于 ListActivity。它提供了几种管理列表视图的方法,用于处理点击事件的 onListItemClick() 回调。

  • PreferenceFragment

    以列表形式显示 Preference 对象的层次结构,类似于 PreferenceActivity。这在为您的应用创建“设置” Activity 时很有用处。

Activity中添加Fragment
  • 静态添加Fragment

    在布局文件里面添加方法如下

<fragment
    android:id="@+id/first_fragment"
    android:name="com.fragment.FirstFragment"
    android:layout_width="match_parent"
    android:layout_height="match_parent">
</fragment>

写demo验证的时候,没有添加id,一直报错如下

 Caused by: java.lang.IllegalArgumentException: Binary XML file line #9: Must specify unique android:id, android:tag, or have a parent with an id for com.fragment.FirstFragment
                                                                    at android.support.v4.app.FragmentManagerImpl.onCreateView(FragmentManager.java:3484)

查看源码,可以看在FragmentManager里面有,这里面id,tag至少有一项不能为空,所以我们静态添加fragment的时候要加上id。(虽然大部分时候都是添加的…)

if (containerId == View.NO_ID && id == View.NO_ID && tag == null) {
    throw new IllegalArgumentException(attrs.getPositionDescription()
            + ": Must specify unique android:id, android:tag, or have a parent with an id for " + fname);
}

注:每个片段都需要一个唯一的标识符,重启 Activity 时,系统可以使用该标识符来恢复片段(您也可以使用该标识符来捕获片段以执行某些事务,如将其移除)。 可以通过三种方式为片段提供 ID:

  • 为 android:id 属性提供唯一 ID。
  • 为 android:tag 属性提供唯一字符串。
  • 如果您未给以上两个属性提供值,系统会使用容器视图的 ID。
  • 动态添加Fragment

    可以在Activity运行期间随时将fragment添加到Activity布局中去,只需要指定将fragment放入哪个ViewGroup。

    具体如何在Activity里面添加移除或者替换fragment,需要使用FragmentTransaction中的API 。通过以下方法获取FragmentTransaction实例

// 获取FragmentTransaction实例
FragmentManager fragmentManager = getFragmentManager();
FragmentTransaction fragmentTransaction = fragmentManager.beginTransaction();
然后可以用add方法去添加一个fragment
FragmentManager manager = getSupportFragmentManager();
FragmentTransaction transaction = manager.beginTransaction();
DynamicFragment fragment1 = new DynamicFragment();
transaction.add(R.id.fragment_container, fragment1);
transaction.commit();

传递到add()的第一个参数是ViewGroup,即应该放置片段的位置,由资源ID指定,第二个参数是要添加的片段,一旦通过FragmentTransaction做出了更改,必须用commit以使更改生效。

执行Fragment

在Activity里面使用fragment最大的优点就是可以根据用户行为执行添加,移除替换。
您可以像下面这样从 FragmentManager 获取一个 FragmentTransaction 实例:

FragmentManager fragmentManager = getFragmentManager();
FragmentTransaction fragmentTransaction = fragmentManager.beginTransaction();

每个事务都是您想要同时执行的一组更改。您可以使用 add()、remove() 和 replace() 等方法为给定事务设置您想要执行的所有更改。然后,要想将事务应用到 Activity,您必须调用 commit()。

不过,在您调用 commit() 之前,您可能想调用 addToBackStack(),以将事务添加到片段事务返回栈。 该返回栈由 Activity 管理,允许用户通过按返回按钮返回上一片段状态。

通过调用 addToBackStack() 可将替换事务保存到返回栈,以便用户能够通过按返回按钮撤消事务并回退到上一片段。

如果您向事务添加了多个更改(如又一个 add() 或 remove()),并且调用了 addToBackStack(),则在调用 commit() 前应用的所有更改都将作为单一事务添加到返回栈,并且返回按钮会将它们一并撤消。

向 FragmentTransaction 添加更改的顺序无关紧要,不过:

您必须最后调用 commit()
如果您要向同一容器添加多个片段,则您添加片段的顺序将决定它们在视图层次结构中的出现顺序
如果您没有在执行移除片段的事务时调用 addToBackStack(),则事务提交时该片段会被销毁,用户将无法回退到该片段。 不过,如果您在删除片段时调用了 addToBackStack(),则系统会停止该片段,并在用户回退时将其恢复。

提示:对于每个片段事务,您都可以通过在提交前调用 setTransition() 来应用过渡动画。

调用 commit() 不会立即执行事务,而是在 Activity 的 UI 线程(“主”线程)可以执行该操作时再安排其在线程上运行。不过,如有必要,您也可以从 UI 线程调用 executePendingTransactions() 以立即执行 commit() 提交的事务。通常不必这样做,除非其他线程中的作业依赖该事务。

注意:您只能在 Activity 保存其状态(用户离开 Activity)之前使用 commit() 提交事务。如果您试图在该时间点后提交,则会引发异常。 这是因为如需恢复 Activity,则提交后的状态可能会丢失。 对于丢失提交无关紧要的情况,请使用 commitAllowingStateLoss()。

与Activity通信

Fragment可以通过getActivity()访问Activity实例,如下

View view = getActivity().findViewById(R.id.list);

同样的,Activity也可以通过findFragentById()或findFragmentByTag(),通过FragmentManager获取对Fragment的引用来调用fragmetn方法。

DynamicFragment fragment = (DynamicFragment) getFragmentManager().findFragmentById(R.id.dynamic_fragment);

下面一个例子是Activity获取fragment里面的值(通过回调)

Fragment里面

// 接口回调
public void getEditText(CallBack callBack){
    //获取文本框信息
    String msg = editText.getText().toString();
    callBack.getResult(msg);
}
// 接口
public interface CallBack{
    // 定义一个获取方法
    public void getResult(String result);
}

Activity里面:

// 使用接口回调的方法获取数据
leftFragment.getEditText(new LeftFragment.CallBack() {
    @Override
    public void getResult(String result) {
        Toast.makeText(CallbackActivity.this, "-->>" + result, Toast.LENGTH_SHORT).show();                    }
    });

Fragment Demo

Fragment生命周期

参考官方文档Fragment生命周期

Loaders

Android3.0开始引入了Loaders,支持轻松在Activity或fragment中异步加载数据。主要有以下特征:

  • 可用于每一个Fragment和Activity

  • 支持异步加载数据

  • 监控其数据源并且在其改变时传递结果

  • 在某一配置更改后重建加载器时,会自动重新连接上一个加载器的游标。 因此,它们无需重新查询其数据

Loader API

在应用中使用加载器

此部分描述如何在 Android 应用中使用加载器。使用加载器的应用通常包括:

  • Activity 或 Fragment。

  • LoaderManager 的实例。

  • 一个 CursorLoader,用于加载由 ContentProvider 支持的数据。您也可以实现自己的 Loader 或 AsyncTaskLoader 子类,从其他源中加载数据。

  • 一个 LoaderManager.LoaderCallbacks 实现。您可以使用它来创建新加载器,并管理对现有加载器的引用

  • 一种显示加载器数据的方法,如 SimpleCursorAdapter。

  • 使用 CursorLoader 时的数据源,如 ContentProvider。

Starting a Loader
getLoaderManager().initLoader(0,null,this);

initLoader()方法采用以下参数:

  • 用于标示加载器唯一ID

  • 在构建时候提供给加载器可选参数

  • LoaderManager.LoaderCallbacks 实现, LoaderManager 将调用此实现来报告加载器事件。在此示例中,本地类实现 LoaderManager.LoaderCallbacks 接口,因此它会传递对自身的引用 this。

initLoader() 调用确保加载器已初始化且处于活动状态。这可能会出现两种结果:

  • 如果 ID 指定的加载器已存在,则将重复使用上次创建的加载器。

  • 如果 ID 指定的加载器不存在,则 initLoader() 将触发 LoaderManager.LoaderCallbacks方法onCreateLoader()。在此方法中,您可以实现代码以实例化并返回新加载器。有关详细介绍,请参阅 onCreateLoader 部分。

无论何种情况,给定的 LoaderManager.LoaderCallbacks 实现均与加载器相关联,且将在加载器状态变化时调用。如果在调用时,调用程序处于启动状态,且请求的加载器已存在并生成了数据,则系统将立即调用 onLoadFinished()(在 initLoader() 期间),因此您必须为此做好准备。 有关此回调的详细介绍,请参阅 onLoadFinished。

请注意,initLoader() 方法将返回已创建的 Loader,但您不必捕获其引用。LoaderManager 将自动管理加载器的生命周期。LoaderManager 将根据需要启动和停止加载,并维护加载器的状态及其相关内容。 这意味着您很少直接与加载器进行交互(有关使用加载器方法调整加载器行为的示例,请参阅 LoaderThrottle 示例)。当特定事件发生时,您通常会使用 LoaderManager.LoaderCallbacks 方法干预加载进程。有关此主题的详细介绍,请参阅使用 LoaderManager 回调

ReStarting a Loader

当您使用 initLoader() 时(如上所述),它将使用含有指定 ID 的现有加载器(如有)。如果没有,则它会创建一个。但有时,您想舍弃这些旧数据并重新开始。

要舍弃旧数据,请使用 restartLoader()。例如,当用户的查询更改时,此 SearchView.OnQueryTextListener 实现将重启加载器。 加载器需要重启,以便它能够使用修订后的搜索过滤器执行新查询:

public boolean onQueryTextChanged(String newText) {
    // Called when the action bar search text has changed.  Update
    // the search filter, and restart the loader to do a new query
    // with this filter.
    mCurFilter = !TextUtils.isEmpty(newText) ? newText : null;
    getLoaderManager().restartLoader(0, null, this);
    return true;
}
Using the LoaderManager Callbacks

LoaderManager.LoaderCallbacks是一个支持客户端与LoaderManager交互的回调接口。

Loaer(特别是CursorLoader)在停止运行后,仍需保留其数据。这样,应用即可保留Activity或fragment的onStop()和onStart()方法中的数据。当用户返回应用时,无需等待它重新加载这些数据。可使用LoaderManager.LoaderCallbacks方法了解何时创建加载器,并告知应用何时停止使用加载器数据

onCreateLoader():针对指定的 ID 进行实例化并返回新的 Loader

当您尝试访问加载器时(例如,通过 initLoader()),该方法将检查是否已存在由该 ID 指定的加载器。 如果没有,它将触发 LoaderManager.LoaderCallbacks 方法 onCreateLoader()。在此方法中,您可以创建新加载器。 通常,这将是 CursorLoader,但您也可以实现自己的 Loader 子类。

在此示例中,onCreateLoader() 回调方法创建了 CursorLoader。您必须使用其构造函数方法来构建 CursorLoader。该方法需要对 ContentProvider 执行查询时所需的一系列完整信息。具体地说,它需要:

  • uri:用于检索内容的 URI
  • projection:要返回的列的列表。传递 null 时,将返回所有列,这样会导致效率低下
  • selection:一种用于声明要返回哪些行的过滤器,采用 SQL WHERE 子句格式(WHERE 本身除外)。传递 null 时,将为指定的 URI 返回所有行
  • selectionArgs:您可以在 selection 中包含 ?s,它将按照在 selection 中显示的顺序替换为 selectionArgs 中的值。该值将绑定为字串符
  • sortOrder:行的排序依据,采用 SQL ORDER BY 子句格式(ORDER BY 自身除外)。传递 null 时,将使用默认排序顺序(可能并未排序)
// If non-null, this is the current filter the user has provided.
String mCurFilter;
...
public Loader<Cursor> onCreateLoader(int id, Bundle args) {
    // This is called when a new Loader needs to be created.  This
    // sample only has one Loader, so we don't care about the ID.
    // First, pick the base URI to use depending on whether we are
    // currently filtering.
    Uri baseUri;
    if (mCurFilter != null) {
        baseUri = Uri.withAppendedPath(Contacts.CONTENT_FILTER_URI,
                  Uri.encode(mCurFilter));
    } else {
        baseUri = Contacts.CONTENT_URI;
    }

    // Now create and return a CursorLoader that will take care of
    // creating a Cursor for the data being displayed.
    String select = "((" + Contacts.DISPLAY_NAME + " NOTNULL) AND ("
            + Contacts.HAS_PHONE_NUMBER + "=1) AND ("
            + Contacts.DISPLAY_NAME + " != '' ))";
    return new CursorLoader(getActivity(), baseUri,
            CONTACTS_SUMMARY_PROJECTION, select, null,
            Contacts.DISPLAY_NAME + " COLLATE LOCALIZED ASC");
}
onLoadFinished() :将在先前创建的加载器完成加载时调用

当先前创建的加载器完成加载时,将调用此方法。该方法必须在为此加载器提供的最后一个数据释放之前调用。 此时,您应移除所有使用的旧数据(因为它们很快会被释放),但不要自行释放这些数据,因为这些数据归其加载器所有,其加载器会处理它们。

当加载器发现应用不再使用这些数据时,即会释放它们。 例如,如果数据是来自 CursorLoader 的一个游标,则您不应手动对其调用 close()。如果游标放置在 CursorAdapter 中,则应使用 swapCursor() 方法,使旧 Cursor 不会关闭。例如:

// This is the Adapter being used to display the list's data.
SimpleCursorAdapter mAdapter;
...

public void onLoadFinished(Loader<Cursor> loader, Cursor data) {
    // Swap the new cursor in.  (The framework will take care of closing the
    // old cursor once we return.)
    mAdapter.swapCursor(data);
}
onLoaderReset():将在先前创建的加载器重置且其数据因此不可用时调用

此方法将在先前创建的加载器重置且其数据因此不可用时调用。 通过此回调,您可以了解何时将释放数据,因而能够及时移除其引用。

此实现调用值为 null 的swapCursor():

// This is the Adapter being used to display the list's data.
SimpleCursorAdapter mAdapter;
...

public void onLoaderReset(Loader<Cursor> loader) {
    // This is called when the last Cursor provided to onLoadFinished()
    // above is about to be closed.  We need to make sure we are no
    // longer using it.
    mAdapter.swapCursor(null);
}
Loaders实例

参考Android Loaders api里面,以下是一个Fragment实例,它展示了一个ListView,其中包含针对联系人内容提供程序的查询结果,它使用CursorLoader管理提供程序的查询。

Android联系人其实很多地方用到了Loaders。

完整LoaderDemo

Loaders参考

可以参考Android Loader机制全面详解及源码浅析

返回栈

这一节主要将一下任务和返回栈,一个应用一般都包含多个Activity,遵循以下"后进先出",如下图
image

下面我们介绍下Activtiy的四种启动模式:

在清单文件种声明Activity的启动模式,使用元素的launchMode属性去指定Activity应该如何与任务关联

  • standard (默认模式)

    默认,系统在启动Activity的任务中创建Activity的新实例并向其传送Intent,Activity可以多次实例化,而每个实例均可属于不用任务,并且一个任务可以拥有多个实例。

  • singleTop

    如果当前任务的顶部已存在 Activity 的一个实例,则系统会通过调用该实例的 onNewIntent() 方法向其传送 Intent,而不是创建 Activity 的新实例。Activity 可以多次实例化,而每个实例均可属于不同的任务,并且一个任务可以拥有多个实例(但前提是位于返回栈顶部的 Activity 并不是 Activity 的现有实例。

    例如,假设任务的返回栈包含根 Activity A 以及 Activity B、C 和位于顶部的 D(堆栈是 A-B-C-D;D 位于顶部)。收到针对 D 类 Activity 的 Intent。如果 D 具有默认的 “standard” 启动模式,则会启动该类的新实例,且堆栈会变成 A-B-C-D-D。但是,如果 D 的启动模式是 “singleTop”,则 D 的现有实例会通过 onNewIntent() 接收 Intent,因为它位于堆栈的顶部;而堆栈仍为 A-B-C-D。但是,如果收到针对 B 类 Activity 的 Intent,则会向堆栈添加 B 的新实例,即便其启动模式为 “singleTop” 也是如此。

    注:为某个 Activity创建新实例时,用户可以按“返回”按钮返回到前一个 Activity。 但是,当 Activity 的现有实例处理新 Intent 时,则在新 Intent 到达 onNewIntent() 之前,用户无法按“返回”按钮返回到 Activity 的状态。

  • singleTask

    系统创建新任务并实例化位于新任务底部的 Activity。但是,如果该 Activity 的一个实例已存在于一个单独的任务中,则系统会通过调用现有实例的 onNewIntent() 方法向其传送 Intent,而不是创建新实例。一次只能存在 Activity 的一个实例。

    singleTask模式下,Task栈中只能有一个对应Activity的实例。例如:现在栈的结构为:A B C D。此时D通过Intent跳转到B,则栈的结构变成了:A B C D B

  • singleInstance

    与 “singleTask” 相同,只是系统不会将任何其他 Activity 启动到包含实例的任务中。该 Activity 始终是其任务唯一仅有的成员;由此 Activity 启动的任何 Activity 均在单独的任务中打开

    singleInstance模式下,会将打开的Activity压入一个新建的任务栈中。例如:Task栈1中结构为:A B C ,C通过Intent跳转到了D(D的模式为singleInstance),那么则会新建一个Task 栈2,栈1中结构依旧为A B C,栈2中结构为D,此时屏幕中显示D,之后D通过Intent跳转到D,栈2中不会压入新的D,所以2个栈中的情况没发生改变。如果D跳转到了C,那么就会根据C对应的launchMode的在栈1中进行对应的操作,C如果为standard,那么D跳转到C,栈1的结构为A B C C ,此时点击返回按钮,还是在C,栈1的结构变为A B C,而不会回到D。

参考

android developer

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值