今天要很任性的研究一下Android5.0中Settings子模块的跳转实现。
我们首先看一下Settings应用的Launcher类。查看package/app/Settings/AndroidManifest.xml文件:
<activity-alias android:name="Settings"
android:taskAffinity="com.android.settings"
android:label="@string/settings_label_launcher"
android:launchMode="singleTask"
android:targetActivity="Settings">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.LAUNCHER" />
intent-filter>
activity-alias>
可以看到,指定的Launcher类为Settings.java,其源码实现如下:
import com.android.settings.applications.AppOpsSummary;
/**
* Top-level Settings activity
*/
public class Settings extends SettingsActivity {
/*
* Settings subclasses for launching independently.
*/
public static class BluetoothSettingsActivity extends SettingsActivity { /* empty */ }
public static class WirelessSettingsActivity extends SettingsActivity { /* empty */ }
public static class SimSettingsActivity extends SettingsActivity { /* empty */ }
public static class TetherSettingsActivity extends SettingsActivity { /* empty */ }
public static class VpnSettingsActivity extends SettingsActivity { /* empty */ }
public static class DateTimeSettingsActivity extends SettingsActivity { /* empty */ }
public static class StorageSettingsActivity extends SettingsActivity { /* empty */ }
public static class WifiSettingsActivity extends SettingsActivity { /* empty */ }
public static class WifiP2pSettingsActivity extends SettingsActivity { /* empty */ }
public static class InputMethodAndLanguageSettingsActivity extends SettingsActivity { /* empty */ }
public static class KeyboardLayoutPickerActivity extends SettingsActivity { /* empty */ }
public static class InputMethodAndSubtypeEnablerActivity extends SettingsActivity { /* empty */ }
public static class VoiceInputSettingsActivity extends SettingsActivity { /* empty */ }
public static class SpellCheckersSettingsActivity extends SettingsActivity { /* empty */ }
public static class LocalePickerActivity extends SettingsActivity { /* empty */ }
public static class UserDictionarySettingsActivity extends SettingsActivity { /* empty */ }
public static class HomeSettingsActivity extends SettingsActivity { /* empty */ }
public static class DisplaySettingsActivity extends SettingsActivity { /* empty */ }
public static class DeviceInfoSettingsActivity extends SettingsActivity { /* empty */ }
public static class ApplicationSettingsActivity extends SettingsActivity { /* empty */ }
public static class ManageApplicationsActivity extends SettingsActivity { /* empty */ }
public static class AppOpsSummaryActivity extends SettingsActivity {
@Override
public boolean isValidFragment(String className) {
if (AppOpsSummary.class.getName().equals(className)) {
return true;
}
return super.isValidFragment(className);
}
}
public static class StorageUseActivity extends SettingsActivity { /* empty */ }
public static class DevelopmentSettingsActivity extends SettingsActivity { /* empty */ }
public static class AccessibilitySettingsActivity extends SettingsActivity { /* empty */ }
public static class CaptioningSettingsActivity extends SettingsActivity { /* empty */ }
public static class AccessibilityInversionSettingsActivity extends SettingsActivity { /* empty */ }
public static class AccessibilityContrastSettingsActivity extends SettingsActivity { /* empty */ }
public static class AccessibilityDaltonizerSettingsActivity extends SettingsActivity { /* empty */ }
public static class SecuritySettingsActivity extends SettingsActivity { /* empty */ }
public static class UsageAccessSettingsActivity extends SettingsActivity { /* empty */ }
public static class LocationSettingsActivity extends SettingsActivity { /* empty */ }
public static class PrivacySettingsActivity extends SettingsActivity { /* empty */ }
public static class RunningServicesActivity extends SettingsActivity { /* empty */ }
public static class ManageAccountsSettingsActivity extends SettingsActivity { /* empty */ }
public static class PowerUsageSummaryActivity extends SettingsActivity { /* empty */ }
public static class BatterySaverSettingsActivity extends SettingsActivity { /* empty */ }
public static class AccountSyncSettingsActivity extends SettingsActivity { /* empty */ }
public static class AccountSettingsActivity extends SettingsActivity { /* empty */ }
public static class AccountSyncSettingsInAddAccountActivity extends SettingsActivity { /* empty */ }
public static class CryptKeeperSettingsActivity extends SettingsActivity { /* empty */ }
public static class DeviceAdminSettingsActivity extends SettingsActivity { /* empty */ }
public static class DataUsageSummaryActivity extends SettingsActivity { /* empty */ }
public static class AdvancedWifiSettingsActivity extends SettingsActivity { /* empty */ }
public static class SavedAccessPointsSettingsActivity extends SettingsActivity { /* empty */ }
public static class TextToSpeechSettingsActivity extends SettingsActivity { /* empty */ }
public static class AndroidBeamSettingsActivity extends SettingsActivity { /* empty */ }
public static class WifiDisplaySettingsActivity extends SettingsActivity { /* empty */ }
public static class DreamSettingsActivity extends SettingsActivity { /* empty */ }
public static class NotificationStationActivity extends SettingsActivity { /* empty */ }
public static class UserSettingsActivity extends SettingsActivity { /* empty */ }
public static class NotificationAccessSettingsActivity extends SettingsActivity { /* empty */ }
public static class ConditionProviderSettingsActivity extends SettingsActivity { /* empty */ }
public static class UsbSettingsActivity extends SettingsActivity { /* empty */ }
public static class TrustedCredentialsSettingsActivity extends SettingsActivity { /* empty */ }
public static class PaymentSettingsActivity extends SettingsActivity { /* empty */ }
public static class PrintSettingsActivity extends SettingsActivity { /* empty */ }
public static class PrintJobSettingsActivity extends SettingsActivity { /* empty */ }
public static class ZenModeSettingsActivity extends SettingsActivity { /* empty */ }
public static class NotificationSettingsActivity extends SettingsActivity { /* empty */ }
public static class NotificationAppListActivity extends SettingsActivity { /* empty */ }
public static class AppNotificationSettingsActivity extends SettingsActivity { /* empty */ }
public static class OtherSoundSettingsActivity extends SettingsActivity { /* empty */ }
public static class QuickLaunchSettingsActivity extends SettingsActivity { /* empty */ }
public static class TopLevelSettings extends SettingsActivity { /* empty */ }
public static class ApnSettingsActivity extends SettingsActivity { /* empty */ }
/**M: Add new for settings activity @{*/
public static class HDMISettingsActivity extends SettingsActivity { /* empty */ }
/**@}*/
}
从源码我们发现,代码里定义了大量的静态内部类,却没有任何跟界面显示相关的内容。
这时候我们不禁会有疑问了,如果我要跳转到Wifi的界面,从源码看应该是WifiDisplaySettingsActivity类,应该如何跳转呢?
我们以Wifi界面为例,首先看一下其在AndroidManifest.xml中是如何定义的?
<activity android:name="Settings$WifiSettingsActivity"
android:taskAffinity=""
android:label="@string/wifi_settings"
android:configChanges="orientation|keyboardHidden|screenSize">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<action android:name="android.settings.WIFI_SETTINGS" />
<category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.VOICE_LAUNCH" />
<category android:name="com.android.settings.SHORTCUT" />
intent-filter>
<meta-data android:name="com.android.settings.FRAGMENT_CLASS"
android:value="com.android.settings.wifi.WifiSettings" />
<meta-data android:name="com.android.settings.TOP_LEVEL_HEADER_ID"
android:resource="@id/wifi_settings" />
activity>
定义的还是蛮复杂的,那我们先写一个简单的demo,通过隐式的Intent看看能不能调起wifi的界面来。
Tips:
为什么不采用显示Intent的方法?是因为Settings的子类我们调用的类一般都不位于同一个包中,所以一般不采用显示调用的方法。
private void startWifiActivity() {
try {
Intent intent = new Intent("android.settings.WIFI_SETTINGS");
startActivity(intent);
} catch (Exception e) {
e.printStackTrace();
}
}
可以看到,我们通过发送一个”android.settings.WIFI_SETTINGS”就已经将Wifi设置界面调度起来了,但是我们之前看SettingsActivity的源码中:
public static class WifiSettingsActivity extends SettingsActivity { /* empty */ }
WifiSettingsActivity的具体实现是空的,那布局到底是如何被渲染出来的呢?我们不禁想到应该是SettingsActivity做了一些手脚。
因为所有的Settings子界面类都是继承自SettingsActivity类,那就让我们以wifi为例,看一下这个SettingsActivity是如何能够正确呈现每个Settings子类的。
在研究SettingsActivity.java的源码之前,让我们先回顾一下WifiSettingsActivity在AndroidManifest.xml中的声明:
<meta-data android:name="com.android.settings.FRAGMENT_CLASS"
android:value="com.android.settings.wifi.WifiSettings" />
其中有meta-data的标签使用,从这个标签的key-value来看,很明显可以认为WifiSettings的具体实现应该是由WifiSettings这个Fragment来布局渲染的。让我们通过源码来分析一下是否是这样的。
SettingActivity的源码如下:
public class SettingsActivity extends Activity
implements PreferenceManager.OnPreferenceTreeClickListener,
PreferenceFragment.OnPreferenceStartFragmentCallback,
ButtonBarHandler, FragmentManager.OnBackStackChangedListener,
SearchView.OnQueryTextListener, SearchView.OnCloseListener,
MenuItem.OnActionExpandListener {
// ......
}
既然SettingsActivity继承自Activity,我们直接从onCreate函数入手就好了。onCreate函数源码如下:
@Override
protected void onCreate(Bundle savedState) {
super.onCreate(savedState);
// Should happen before any call to getIntent()
getMetaData();
}
这里有个获取MetaData的方法,而且通过注释我们可以得到信息:这个方法应该再调用任何getIntent()方法之前进行调用。那我们来看一下这个方法的具体实现:
private static final String META_DATA_KEY_FRAGMENT_CLASS =
"com.android.settings.FRAGMENT_CLASS";
private String mFragmentClass;
private void getMetaData() {
try {
ActivityInfo ai = getPackageManager().getActivityInfo(getComponentName(),
PackageManager.GET_META_DATA);
if (ai == null || ai.metaData == null) return;
mFragmentClass = ai.metaData.getString(META_DATA_KEY_FRAGMENT_CLASS);
} catch (NameNotFoundException nnfe) {
// No recovery
Log.d(LOG_TAG, "Cannot get Metadata for: " + getComponentName().toString());
}
}
可以看到,这个函数的主要作用就是从Activity标签中获取meta-data标签中key为com.android.settings.FRAGMENT_CLASS的值,并将其赋值给mFragmentClass这个私有变量。
以WifiSettingsActivity为例,从这个Activity中meta-data标签中获取的信息为com.android.settings.wifi.WifiSettings,即mFragmentClass=”com.android.settings.wifi.WifiSettings”。
既然mFragmentClass已经指定为具体Fragment的路径,那我们回到onCreate函数,看一下activity是如何加载fragment的。
protected void onCreate(Bundle savedState) {
final Intent intent = getIntent();
}
上面注释说getMetaData()要在getIntent()函数之前执行,那我们就看一下getIntent的实现,来了解为什么会有这样的规定。getIntent源码如下:
@Override
public Intent getIntent() {
Intent superIntent = super.getIntent();
// 处理个别特殊情况,本例就是将mFragmentClass赋值给了startingFragment
String startingFragment = getStartingFragmentClass(superIntent);
// This is called from super.onCreate, isMultiPane() is not yet reliable
// Do not use onIsHidingHeaders either, which relies itself on this method
if (startingFragment != null) {
Intent modIntent = new Intent(superIntent);
// 将startingFragment放入intent的以EXTRA_SHOW_FRAGMENT(":settings:show_fragment")为key的键值对中。
modIntent.putExtra(EXTRA_SHOW_FRAGMENT, startingFragment);
Bundle args = superIntent.getExtras();
if (args != null) {
args = new Bundle(args);
} else {
args = new Bundle();
}
args.putParcelable("intent", superIntent);
modIntent.putExtra(EXTRA_SHOW_FRAGMENT_ARGUMENTS, args);
return modIntent;
}
return superIntent;
}
private String getStartingFragmentClass(Intent intent) {
if (mFragmentClass != null) return mFragmentClass;
String intentClass = intent.getComponent().getClassName();
if (intentClass.equals(getClass().getName())) return null;
if ("com.android.settings.ManageApplications".equals(intentClass)
|| "com.android.settings.RunningServices".equals(intentClass)
|| "com.android.settings.applications.StorageUse".equals(intentClass)) {
// Old names of manage apps.
intentClass = com.android.settings.applications.ManageApplications.class.getName();
}
return intentClass;
}
从源码看以看出,getIntent的作用就是构造了一个Intent,并且给它增加了一个特殊的键值对,key为”:settings:show_fragment”,value为mFragmentClass指定的Fragment类名。
之所以要先执行getMetaData,是因为mFragmentClass赋值是在getMeatData中进行的。
分析了getIntent,我们继续来看onCreate函数,看看这个intent到底是如何被使用的。
protected void onCreate(Bundle savedState) {
final ComponentName cn = intent.getComponent();
final String className = cn.getClassName(); // 本例中,className为WifiSettingsActivity
mIsShowingDashboard = className.equals(Settings.class.getName()); // 该值为false
setContentView(mIsShowingDashboard ?
R.layout.settings_main_dashboard : R.layout.settings_main_prefs); // 由于mIsShowingDashboard为false,所以WifiSettingsActivity加载的布局为R.layout.settings_main_prefs
mContent = (ViewGroup) findViewById(R.id.main_content); // 获取承载Fragment的ViewGroup
}
我们来看一下R.layout.settings_main_prefs布局实现:
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:orientation="vertical"
android:layout_height="match_parent"
android:layout_width="match_parent">
<LinearLayout
android:orientation="vertical"
android:layout_height="0px"
android:layout_width="match_parent"
android:layout_weight="1">
<com.android.settings.widget.SwitchBar android:id="@+id/switch_bar"
android:layout_height="?android:attr/actionBarSize"
android:layout_width="match_parent"
android:background="@drawable/switchbar_background"
android:theme="?attr/switchBarTheme"
/>
<FrameLayout
android:id="@+id/main_content"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="#ff000000"
/>
LinearLayout>
<RelativeLayout android:id="@+id/button_bar"
android:layout_height="wrap_content"
android:layout_width="match_parent"
android:layout_weight="0"
android:visibility="gone">
<Button android:id="@+id/back_button"
android:layout_width="150dip"
android:layout_height="wrap_content"
android:layout_margin="5dip"
android:layout_alignParentStart="true"
android:text="@*android:string/back_button_label"
/>
<LinearLayout
android:orientation="horizontal"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentEnd="true">
<Button android:id="@+id/skip_button"
android:layout_width="150dip"
android:layout_height="wrap_content"
android:layout_margin="5dip"
android:text="@*android:string/skip_button_label"
android:visibility="gone"
/>
<Button android:id="@+id/next_button"
android:layout_width="150dip"
android:layout_height="wrap_content"
android:layout_margin="5dip"
android:text="@*android:string/next_button_label"
/>
LinearLayout>
RelativeLayout>
LinearLayout>
布局结构还是比较清晰的,我们继续研究onCreate方法。
protected void onCreate(Bundle savedState) {
if (savedState != null) {
// 这里为null,只需要关注else实现即可
} else {
if (!mIsShowingDashborad) {
// 之前分析过本例中mIsShowingDashborad为true,所以看这个分支流程
// 获取标题
setTitleFromIntent(intent);
// 这里给initialArguments赋值为之前设置的mFragmentClass的值
Bundle initialArguments = intent.getBundleExtra(EXTRA_SHOW_FRAGMENT_ARGUMENTS);
switchToFragment(initialFragmentName, initialArguments, true, false,
mInitialTitleResId, mInitialTitle, false);
}
}
}
从上面的源码,我们终于找到了关键部分,initialArguments通过赋值保存了meta-data中指定的com.android.settings.wifi.WifiSettings,我们就看一下switchToFragment的实现。
/**
* Switch to a specific Fragment with taking care of validation, Title and BackStack
*/
private Fragment switchToFragment(String fragmentName, Bundle args, boolean validate,
boolean addToBackStack, int titleResId, CharSequence title, boolean withTransition) {
// 判断fragment是否是合法的类
if (validate && !isValidFragment(fragmentName)) {
throw new IllegalArgumentException("Invalid fragment for this activity: "
+ fragmentName);
}
// 实例化fragment
Fragment f = Fragment.instantiate(this, fragmentName, args);
FragmentTransaction transaction = getFragmentManager().beginTransaction();
// 通过FragmentTransaction的replace方法,讲Fragment的布局在R.id.main_content指定的位置进行渲染
transaction.replace(R.id.main_content, f);
if (withTransition) {
TransitionManager.beginDelayedTransition(mContent);
}
if (addToBackStack) {
transaction.addToBackStack(SettingsActivity.BACK_STACK_PREFS);
}
if (titleResId > 0) {
transaction.setBreadCrumbTitle(titleResId);
} else if (title != null) {
transaction.setBreadCrumbTitle(title);
}
transaction.commitAllowingStateLoss();
getFragmentManager().executePendingTransactions();
return f;
}
好了,通过上述分析,相信大家已经清楚如何通过隐式的intent跳转到其对应的Activity布局了。
通过上面的分析,我们应该了解的事情是,AndroidManifest中每个Activity其meta-data中的数据都是很有用的,特别是com.android.settings.FRAGMENT_CLASS对应的键值对,指定了其所在Activity的真正布局实现。
其实,也是完成了从其他包的Activity向Settings中fragment的跳转实现。