Setting作为安卓一个比较重要的系统级应用,为用户提供一些系统项的设置。原生android系统的源码路径:/packages/apps/Settings。但MTK厂商的源码包中对该应用进行了重构其源码路径:/vendor/mediatek/proprietary/packages/apps/MtkSettings

一、Setting

1、入口Activity

android应用程序的入口比较简单,可以直接查看AndroidManifest.xml,里面有配置应用的包名、版本、权限、四大组件等。Setting配置文件代码如下:

<!--packages/apps/Settings/AndroidManifest.xml-->
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
        xmlns:androidprv="http://schemas.android.com/apk/prv/res/android"
        package="com.android.settings"
        coreApp="true"
        android:sharedUserId="android.uid.system">           <!--标识系统应用-->
    <!--包名-->
    <original-package android:name="com.android.settings" />  
    <!--应用配置-->
    <application android:label="@string/settings_label"
            android:icon="@drawable/ic_launcher_settings"
            android:theme="@style/Theme.Settings"
            android:hardwareAccelerated="true"
            android:requiredForAllUsers="true"
            android:supportsRtl="true"
            android:backupAgent="com.android.settings.backup.SettingsBackupHelper"
            android:usesCleartextTraffic="true"
            android:defaultToDeviceProtectedStorage="true"
            android:directBootAware="true"
            android:appComponentFactory="androidx.core.app.CoreComponentFactory">
        <uses-library android:name="org.apache.http.legacy" />
        <!-- Settings -->
        <activity android:name=".homepage.SettingsHomepageActivity"
                  android:label="@string/settings_label_launcher"
                  android:theme="@style/Theme.Settings.Home"
                  android:taskAffinity="com.android.settings.root"
                  android:launchMode="singleTask"
                  android:configChanges="keyboard|keyboardHidden">
            <intent-filter android:priority="1">
                <action android:name="android.settings.SETTINGS" />
                <category android:name="android.intent.category.DEFAULT" />
            </intent-filter>
            <meta-data android:name="com.android.settings.PRIMARY_PROFILE_CONTROLLED" android:value="true" />
        </activity>
        <!--主界面,activity的别名,目标actvity是SettingsHomepageActivity-->
        <activity-alias android:name="Settings"
                android:label="@string/settings_label_launcher"
                android:taskAffinity="com.android.settings.root"
                android:launchMode="singleTask"
                android:targetActivity=".homepage.SettingsHomepageActivity">
            <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>
            <meta-data android:name="android.app.shortcuts" android:resource="@xml/shortcuts"/>
        </activity-alias>
        <receiver android:name=".SettingsInitialize">
            <intent-filter>
                <action android:name="android.intent.action.USER_INITIALIZE"/>
                <action android:name="android.intent.action.PRE_BOOT_COMPLETED"/>
            </intent-filter>
        </receiver>
        <activity android:name=".SubSettings"/>
        <!--activity配置 省略......-->
    </application>
</manifest>

上面的配置文件第一个被android.intent.action.MAIN修饰的是一个activity-alias标签,该标签意思是一个activity的别名。即它是一个已经存在的activity的别名(这个已经存在的activity被属性targetActivity修饰),因此我们可以认为Settings程序的主界面是SettingsHomepageActivity.java。值得注意的是该标签的name属性并不会指定某个java文件,它只是一个命名标志而已,与他绑定相关的java文件是targetActivity对应的内容,因此这里只是把主界面命名为Settings,但是与源代码中的com.android.settings.Settings.java毫无关系。详情参考《activity-alias详解及应用》

为了验证上面的理论如上图我分别在SettingsHomepageActivity.java和Settings.java的onCreate加了日志,在启动Settings的时候看看打印的日志是什么?

2、SettingsHomepageActivity

//packages/apps/Settings/src/com/android/settings/homepage/SettingsHomepageActivity.java
public class SettingsHomepageActivity extends FragmentActivity {
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        // 设置主界面布局文件:settings_homepage_container.xml
        setContentView(R.layout.settings_homepage_container);
        // 获取布局文件中homepage_container:主题内容
        final View root = findViewById(R.id.settings_homepage_container);
        root.setSystemUiVisibility(View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION |  View.SYSTEM_UI_FLAG_LAYOUT_STABLE);
        //获取布局文件中search_action_bar:顶部搜索控件
        final Toolbar toolbar = findViewById(R.id.search_action_bar);
        FeatureFactory.getFactory(this).getSearchFeatureProvider().initSearchToolbar(this, toolbar, SettingsEnums.SETTINGS_HOMEPAGE);
        //设置contextual_cards_content对应的fragment,不是低内存手机显示卡片布局视图
        if (!getSystemService(ActivityManager.class).isLowRamDevice()) {
            showFragment(new ContextualCardsFragment(), R.id.contextual_cards_content);
        }
        //设置main_content对应fragment为TopLevelSettings,即所有二级菜单选项
        showFragment(new TopLevelSettings(), R.id.main_content);
        ((FrameLayout) findViewById(R.id.main_content))
                .getLayoutTransition().enableTransitionType(LayoutTransition.CHANGING);
    }
}
<!--packages/apps/Settings/res/layout/settings_homepage_container.xml-->
<androidx.coordinatorlayout.widget.CoordinatorLayout
    xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    android:id="@+id/settings_homepage_container"
    android:fitsSystemWindows="true"
    android:layout_width="match_parent"
    android:layout_height="match_parent">
    <androidx.core.widget.NestedScrollView
        android:id="@+id/main_content_scrollable_container"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        app:layout_behavior="com.android.settings.widget.FloatingAppBarScrollingViewBehavior">
        <!--主题内容布局-->
        <LinearLayout
            android:id="@+id/homepage_container"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:orientation="vertical">
            <!--显示一些通知卡片-->
            <FrameLayout
                android:id="@+id/contextual_cards_content"
                android:layout_width="match_parent"
                android:layout_height="wrap_content"
                android:layout_marginStart="@dimen/contextual_card_side_margin"
                android:layout_marginEnd="@dimen/contextual_card_side_margin"/>
            <!--所有的二级菜单-->
            <FrameLayout
                android:id="@+id/main_content"
                android:layout_width="match_parent"
                android:layout_height="wrap_content"
                android:animateLayoutChanges="true"
                android:background="?android:attr/windowBackground"/>
        </LinearLayout>
    </androidx.core.widget.NestedScrollView>
    <!--顶部搜索栏-->
    <com.google.android.material.appbar.AppBarLayout
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:touchscreenBlocksFocus="false"
        android:keyboardNavigationCluster="false">
        <include layout="@layout/search_bar"/>
    </com.google.android.material.appbar.AppBarLayout>
</androidx.coordinatorlayout.widget.CoordinatorLayout>

如上Activity和对应的布局文件代码,很容易理解,整个布局主要分两大块:

  • 首先顶部的搜索栏通过包含search_bar.xml文件(实现了Settings的搜索设置功能),通过工厂方式来创建并初始化SearchFeatureProvider,可通过它来搜索Settings所有的子菜单
  • 最后内容主体部分使用了NestedScrollView滑动控件,它包含了两个部分:顶部的卡片内容和下面主体二级菜单选项。其中二级菜单选项视图被设置成了TopLevelSettings

2.1、SearchFeatureProvider搜索子菜单

2.2、TopLevelSettings二级菜单布局

//packages/apps/Settings/src/com/android/settings/homepage/TopLevelSettings.java
@SearchIndexable(forTarget = MOBILE)
public class TopLevelSettings extends DashboardFragment implements PreferenceFragmentCompat.OnPreferenceStartFragmentCallback {
    public TopLevelSettings() {
        final Bundle args = new Bundle();
        args.putBoolean(NEED_SEARCH_ICON_IN_ACTION_BAR, false); //是否禁用搜索图标
        setArguments(args);
    }
    @Override
    protected int getPreferenceScreenResId() {
        return R.xml.top_level_settings;  //对于视图布局文件
    }
    @Override
    public void onAttach(Context context) {
        super.onAttach(context);
        use(SupportPreferenceController.class).setActivity(getActivity());
    }
    @Override
    public boolean onPreferenceStartFragment(PreferenceFragmentCompat caller, Preference pref) {
        new SubSettingLauncher(getActivity())
                .setDestination(pref.getFragment())
                .setArguments(pref.getExtras())
                .setSourceMetricsCategory(caller instanceof Instrumentable ? ((Instrumentable) caller).getMetricsCategory() : Instrumentable.METRICS_CATEGORY_UNKNOWN)
                .setTitleRes(-1)
                .launch();
        return true;
    }
    @Override
    protected boolean shouldForceRoundedIcon() {
        return getContext().getResources()
                .getBoolean(R.bool.config_force_rounded_icon_TopLevelSettings);
    }
}

从上面的代码可以知道TopLevelSettings是一个fragment,大部分功能在DashboardFragment父类具体实现,这里只提供了几个接口,从名字上可以看出getPreferenceScreenResId对应于视图布局文件,top_level_settings.xml如下:

2.3、Preference偏好设置

如上小节,我们发现布局文件中在PreferenceScreen里面包含了一批Preference。这个是什么东西呢?其实Preference是android原生为了持久化数据的一种"控件",注意它并不是真正意义上的View。具体用法可以参考《Android之PreferenceFragment详解》

  • Preference家族

Preference源码路径如下图,其中比较重要的有前面已经见过的PreferenceScreen和Preference。

  • Preference是"控件"?

Preference并不是控件,它没有继承View,但是为什么我们的布局文件中可以使用他呢?其实只有继承了PreferenceFragment或者PreferenceActivity的才能使用上面以PreferenceScreen标签开头的xml文件,因为他们内部作了一系列解析,同时Preference有方法返回一个View对象,如下代码:

//android/frameworks/base/core/java/android/preference/Preference.java
@Deprecated
public class Preference implements Comparable<Preference> {
    private boolean mShouldDisableView = true;
    @UnsupportedAppUsage
    private int mLayoutResId = com.android.internal.R.layout.preference;
    @UnsupportedAppUsage
    private int mWidgetLayoutResId;
    public void setLayoutResource(@LayoutRes int layoutResId) {
        if (layoutResId != mLayoutResId)   mRecycleEnabled = false;
        mLayoutResId = layoutResId;
    }
    @LayoutRes
    public int getLayoutResource() {
        return mLayoutResId;
    }
    //返回一个视图控件View
    public View getView(View convertView, ViewGroup parent) {
        //如果第一次就创建视图View
        if (convertView == null)  convertView = onCreateView(parent);
        //给视图View填充数据和更新ui
        onBindView(convertView);
        return convertView;
    }
    //创建视图控件View,实际上还是inflate对应的资源ID文件
    @CallSuper
    protected View onCreateView(ViewGroup parent) {
        final LayoutInflater layoutInflater = (LayoutInflater) mContext.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
        final View layout = layoutInflater.inflate(mLayoutResId, parent, false);
        final ViewGroup widgetFrame = (ViewGroup) layout.findViewById(com.android.internal.R.id.widget_frame);
       if (widgetFrame != null) {
            if (mWidgetLayoutResId != 0) layoutInflater.inflate(mWidgetLayoutResId, widgetFrame);
            else widgetFrame.setVisibility(View.GONE);
        }
        return layout;
    }
    //主要是给上面创建的视图View设置数据
    @CallSuper
    protected void onBindView(View view) {
        final TextView titleView = (TextView) view.findViewById(com.android.internal.R.id.title);
        if (titleView != null) {
            final CharSequence title = getTitle();
            if (!TextUtils.isEmpty(title)) {
                titleView.setText(title);
                titleView.setVisibility(View.VISIBLE);
                if (mHasSingleLineTitleAttr) titleView.setSingleLine(mSingleLineTitle);
            } else {
                titleView.setVisibility(View.GONE);
            }
        }
        final TextView summaryView = (TextView) view.findViewById( com.android.internal.R.id.summary);
        if (summaryView != null) {
            final CharSequence summary = getSummary();
            if (!TextUtils.isEmpty(summary)) {
                summaryView.setText(summary);
                summaryView.setVisibility(View.VISIBLE);
            } else {
                summaryView.setVisibility(View.GONE);
            }
        }
        final ImageView imageView = (ImageView) view.findViewById(com.android.internal.R.id.icon);
        if (imageView != null) {
            if (mIconResId != 0 || mIcon != null) {
                if (mIcon == null)  mIcon = getContext().getDrawable(mIconResId);
                if (mIcon != null) imageView.setImageDrawable(mIcon);
            }
            if (mIcon != null) imageView.setVisibility(View.VISIBLE);
            else imageView.setVisibility(mIconSpaceReserved ? View.INVISIBLE : View.GONE);
        }
        final View imageFrame = view.findViewById(com.android.internal.R.id.icon_frame);
        if (imageFrame != null) {
            if (mIcon != null) imageFrame.setVisibility(View.VISIBLE);
            else imageFrame.setVisibility(mIconSpaceReserved ? View.INVISIBLE : View.GONE);
        }
        if (mShouldDisableView) setEnabledStateOnViews(view, isEnabled());
    }
    //界面更改条目栏
    public void setTitle(CharSequence title) {
        if (title == null && mTitle != null || title != null && !title.equals(mTitle)) {
            mTitleRes = 0;
            mTitle = title;
            notifyChanged();
        }
    }
    //界面更改图标
    public void setIcon(Drawable icon) {
        if ((icon == null && mIcon != null) || (icon != null && mIcon != icon)) {
            mIcon = icon;
            notifyChanged();
        }
    }
    //界面有改变回调监听器
    protected void notifyChanged() {
        if (mListener != null)  mListener.onPreferenceChange(this);
    }
}
  • Preference如何持久化?

Settings为什么大量使用了Preference,而没有使用到我们常见的View和TextView呢,因为这里逻辑功能上主要是为了给系统进行一些设置,所有就涉及到了设置参数持久化(即断电后还继续生效),如下代码它内部已经通过PreferenceManager来进行对数据的持久化,实际上还是使用了四大存储方式之一。

//android/frameworks/base/core/java/android/preference/Preference.java
@Deprecated
public class Preference implements Comparable<Preference> {
    @Nullable
    private PreferenceManager mPreferenceManager;
    public SharedPreferences getSharedPreferences() {
        if (mPreferenceManager == null || getPreferenceDataStore() != null)   return null;
        return mPreferenceManager.getSharedPreferences();
    }
    public SharedPreferences.Editor getEditor() {
        if (mPreferenceManager == null || getPreferenceDataStore() != null)  return null;
        return mPreferenceManager.getEditor();
    }
    public boolean shouldCommit() {
        if (mPreferenceManager == null)  return false;
        return mPreferenceManager.shouldCommit();
    }
    public PreferenceManager getPreferenceManager() {
        return mPreferenceManager;
    }
}
  • Preference唯一标识
  • Preference如何自动跳转android:fragment?

 在阅读Android系统应用Settings源码的时候,我们会发现它的布局文件有一大批Preference控件,这个是什么东西呢?其实Preference是android原生为了持久化数据的一种"控件",注意它并不是真正意义上的View。

二、深度剖析Preference

Preference家族代码被定义在frameworks/base/core/java/android/preference,改目录下的代码用来实现偏好设置相关的界面。即Preference是android原生为了持久化数据的一种"控件"。要使用Preference就必须建立在PreferenceActivity或PreferenceFragment的载体上面。具体用法可以参考《Android之PreferenceFragment详解》

  • Preference家族

Preference被定义在frameworks/base/core下面,其中Preference作为视图控件在整个家族中的地位类似于View,PreferenceGroup就类似于ViewGroup,XXXPreference则继承于Preference并扩展了不同功能样式。除此之外Preference并不能直接使用在Activity和Fragment上面,因此多了PreferenceActivity和PreferenceFragment他们分别继承Activity和Fragment并在此基础上实现了数据持久化、控件解析等功能。除此之外还有两个比较重要的类: PreferenceScreenPreferenceGroupAdapter

  • Preference是"控件"?

Preference并不是控件,它没有继承View,但是为什么我们的布局文件中可以使用他呢?其实只有继承了PreferenceFragment或者PreferenceActivity的才能使用上面以PreferenceScreen标签开头的xml文件,因为他们内部作了一系列解析,同时Preference有方法返回一个View对象,如下代码:

//android/frameworks/base/core/java/android/preference/Preference.java
@Deprecated
public class Preference implements Comparable<Preference> {
    private boolean mShouldDisableView = true;
    @UnsupportedAppUsage
    private int mLayoutResId = com.android.internal.R.layout.preference;
    @UnsupportedAppUsage
    private int mWidgetLayoutResId;
    public void setLayoutResource(@LayoutRes int layoutResId) {
        if (layoutResId != mLayoutResId)   mRecycleEnabled = false;
        mLayoutResId = layoutResId;
    }
    @LayoutRes
    public int getLayoutResource() {
        return mLayoutResId;
    }
    //返回一个视图控件View
    public View getView(View convertView, ViewGroup parent) {
        //如果第一次就创建视图View
        if (convertView == null)  convertView = onCreateView(parent);
        //给视图View填充数据和更新ui
        onBindView(convertView);
        return convertView;
    }
    //创建视图控件View,实际上还是inflate对应的资源ID文件
    @CallSuper
    protected View onCreateView(ViewGroup parent) {
        final LayoutInflater layoutInflater = (LayoutInflater) mContext.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
        final View layout = layoutInflater.inflate(mLayoutResId, parent, false);
        final ViewGroup widgetFrame = (ViewGroup) layout.findViewById(com.android.internal.R.id.widget_frame);
       if (widgetFrame != null) {
            if (mWidgetLayoutResId != 0) layoutInflater.inflate(mWidgetLayoutResId, widgetFrame);
            else widgetFrame.setVisibility(View.GONE);
        }
        return layout;
    }
    //主要是给上面创建的视图View设置数据
    @CallSuper
    protected void onBindView(View view) {
        final TextView titleView = (TextView) view.findViewById(com.android.internal.R.id.title);
        if (titleView != null) {
            final CharSequence title = getTitle();
            if (!TextUtils.isEmpty(title)) {
                titleView.setText(title);
                titleView.setVisibility(View.VISIBLE);
                if (mHasSingleLineTitleAttr) titleView.setSingleLine(mSingleLineTitle);
            } else {
                titleView.setVisibility(View.GONE);
            }
        }
        final TextView summaryView = (TextView) view.findViewById( com.android.internal.R.id.summary);
        if (summaryView != null) {
            final CharSequence summary = getSummary();
            if (!TextUtils.isEmpty(summary)) {
                summaryView.setText(summary);
                summaryView.setVisibility(View.VISIBLE);
            } else {
                summaryView.setVisibility(View.GONE);
            }
        }
        final ImageView imageView = (ImageView) view.findViewById(com.android.internal.R.id.icon);
        if (imageView != null) {
            if (mIconResId != 0 || mIcon != null) {
                if (mIcon == null)  mIcon = getContext().getDrawable(mIconResId);
                if (mIcon != null) imageView.setImageDrawable(mIcon);
            }
            if (mIcon != null) imageView.setVisibility(View.VISIBLE);
            else imageView.setVisibility(mIconSpaceReserved ? View.INVISIBLE : View.GONE);
        }
        final View imageFrame = view.findViewById(com.android.internal.R.id.icon_frame);
        if (imageFrame != null) {
            if (mIcon != null) imageFrame.setVisibility(View.VISIBLE);
            else imageFrame.setVisibility(mIconSpaceReserved ? View.INVISIBLE : View.GONE);
        }
        if (mShouldDisableView) setEnabledStateOnViews(view, isEnabled());
    }
    //界面更改条目栏
    public void setTitle(CharSequence title) {
        if (title == null && mTitle != null || title != null && !title.equals(mTitle)) {
            mTitleRes = 0;
            mTitle = title;
            notifyChanged();
        }
    }
    //界面更改图标
    public void setIcon(Drawable icon) {
        if ((icon == null && mIcon != null) || (icon != null && mIcon != icon)) {
            mIcon = icon;
            notifyChanged();
        }
    }
    //界面有改变回调监听器
    protected void notifyChanged() {
        if (mListener != null)  mListener.onPreferenceChange(this);
    }
}
  • Preference如何持久化?

Settings为什么大量使用了Preference,而没有使用到我们常见的View和TextView呢,因为这里逻辑功能上主要是为了给系统进行一些设置,所有就涉及到了设置参数持久化(即断电后还继续生效),如下代码它内部已经通过PreferenceManager来进行对数据的持久化,实际上还是使用了四大存储方式之一

//android/frameworks/base/core/java/android/preference/Preference.java
@Deprecated
public class Preference implements Comparable<Preference> {
    @Nullable
    private PreferenceManager mPreferenceManager;
    public SharedPreferences getSharedPreferences() {
        if (mPreferenceManager == null || getPreferenceDataStore() != null)   return null;
        return mPreferenceManager.getSharedPreferences();
    }
    public SharedPreferences.Editor getEditor() {
        if (mPreferenceManager == null || getPreferenceDataStore() != null)  return null;
        return mPreferenceManager.getEditor();
    }
    public boolean shouldCommit() {
        if (mPreferenceManager == null)  return false;
        return mPreferenceManager.shouldCommit();
    }
    public PreferenceManager getPreferenceManager() {
        return mPreferenceManager;
    }
}
  • Preference唯一标识
  • Preference跳转到Fragment

上面初步介绍了Preference的基本用法和一些特点,想使用Preference必须建立在PreferenceActivity或者PreferenceFragment的基础之上,除此之外布局文件最外层必须使用一个PreferenceScreen嵌套所有的Preference。

1、剖析PreferenceActivity

1.1、布局文件加载

PreferenceActivity的布局跟主流布局类似,即通常由三部分组成:标题栏、内容、最底层的bar。这里我们关注标题栏和内容,因为PreferenceActivity在设计的时候就考虑到了公用同一个activity进行页面跳转,即替换内容区域的fragment。

//frameworks/base/core/java/android/preference/PreferenceActivity.java
/*继承ListActivity,即其布局文件中必须有一个ListView控件且id必须为list*/
@Deprecated public abstract class PreferenceActivity extends ListActivity implements
        /*Preference树结构下所有视图控件点击回调事件*/
        PreferenceManager.OnPreferenceTreeClickListener,
        /*Preference被点击后如果有设置fragment就直接替换fragment回调*/
        PreferenceFragment.OnPreferenceStartFragmentCallback {
    //通过android:fragment属性进行跳转不同fragment页面,可能共用同一个PreferenceActivity,但是标题栏不一样,mHeaders 存储了所有的标题
    private final ArrayList<Header> mHeaders = new ArrayList<Header>();
    private FrameLayout mListFooter;
    private ViewGroup mPrefsContainer;
    private CharSequence mActivityTitle;
    private ViewGroup mHeadersContainer;
    //当前对应的标题,Header表示一个标题
    private Header mCurHeader;
    //管理器
    private PreferenceManager mPreferenceManager;
    //数据持久化
    private Bundle mSavedInstanceState;
    @Override
    protected void onCreate(@Nullable Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        //布局文件加载的是preference_list_content.xml
        final int layoutResId = sa.getResourceId(
                com.android.internal.R.styleable.PreferenceActivity_layout,
                com.android.internal.R.layout.preference_list_content);
        setContentView(layoutResId);
        mListFooter = (FrameLayout)findViewById(com.android.internal.R.id.list_footer);
        mPrefsContainer = (ViewGroup) findViewById(com.android.internal.R.id.prefs_frame);
        mHeadersContainer = (ViewGroup) findViewById(com.android.internal.R.id.headers);
        //......
    }
}
//frameworks/base/core/res/res/layout/preference_list_content.xml
<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:id="@+id/prefs_container"
        android:orientation="horizontal"
        android:layout_width="match_parent"
        android:layout_height="0px"
        android:layout_weight="1">
        <!--标题栏集合headers:通常情况只会显示一个header,其他header被隐藏-->
        <LinearLayout
            style="?attr/preferenceHeaderPanelStyle"
            android:id="@+id/headers"
            android:orientation="vertical"
            android:layout_width="0px"
            android:layout_height="match_parent"
            android:layout_weight="@integer/preferences_left_pane_weight">
            <ListView android:id="@android:id/list"
                style="?attr/preferenceListStyle"
                android:layout_width="match_parent"
                android:layout_height="0px"
                android:layout_weight="1"
                android:clipToPadding="false"
                android:drawSelectorOnTop="false"
                android:cacheColorHint="@android:color/transparent"
                android:listPreferredItemHeight="48dp"
                android:scrollbarAlwaysDrawVerticalTrack="true" />
            <FrameLayout android:id="@+id/list_footer"
                    android:layout_width="match_parent"
                    android:layout_height="wrap_content"
                    android:layout_weight="0" />
        </LinearLayout>
        <!--内容fragment:其中PreferenceFrameLayout代表一个PreferenceScreen布局文件,在点击选项时通过android:fragment进行替换到对应的PreferenceFragment界面-->
        <LinearLayout
                android:id="@+id/prefs_frame"
                style="?attr/preferencePanelStyle"
                android:layout_width="0px"
                android:layout_height="match_parent"
                android:layout_weight="@integer/preferences_right_pane_weight"
                android:orientation="vertical">
            <include layout="@layout/breadcrumbs_in_fragment" />
            <android.preference.PreferenceFrameLayout android:id="@+id/prefs"
                    android:layout_width="match_parent"
                    android:layout_height="0dip"
                    android:layout_weight="1"
                />
        </LinearLayout>
    </LinearLayout>
    <!--省略............-->
</LinearLayout>

1.2、Header对应fragment的标题

//frameworks/base/core/java/android/preference/PreferenceActivity.java
@Deprecated public abstract class PreferenceActivity extends ListActivity{
    //Header对应当前界面,即存储了当前界面的基本信息:
    //                   titleRes和title为标题栏
    //                   summaryRes和summary为概述
    @Deprecated public static final class Header implements Parcelable {
        @StringRes
        public int titleRes;
        public CharSequence title;
        @StringRes
        public int summaryRes;
        public CharSequence summary;
        //对应的ICON图标
        public int iconRes;
        //存储了对应的fragment的类名,点击Preference会自动跳转到android:fragment属性的值对应的类(通过反射进行实例化)
        public String fragment;
        //需要传递到fragment的参数
        public Bundle fragmentArguments;
        public Intent intent;
        public Bundle extras;
        //序列化上面的内容,包括fragment的类名
        @Override public void writeToParcel(Parcel dest, int flags) {
            dest.writeLong(id);
            dest.writeInt(titleRes);
            TextUtils.writeToParcel(title, dest, flags);
            dest.writeInt(summaryRes);
            TextUtils.writeToParcel(summary, dest, flags);
            dest.writeInt(breadCrumbTitleRes);
            TextUtils.writeToParcel(breadCrumbTitle, dest, flags);
            dest.writeInt(breadCrumbShortTitleRes);
            TextUtils.writeToParcel(breadCrumbShortTitle, dest, flags);
            dest.writeInt(iconRes);
            dest.writeString(fragment);
            dest.writeBundle(fragmentArguments);
            if (intent != null) {
                dest.writeInt(1);
                intent.writeToParcel(dest, flags);
            } else dest.writeInt(0);
            dest.writeBundle(extras);
        }
        //反序列化上面的内容,包括fragment的类名
        public void readFromParcel(Parcel in) {
            id = in.readLong();
            titleRes = in.readInt();
            title = TextUtils.CHAR_SEQUENCE_CREATOR.createFromParcel(in);
            summaryRes = in.readInt();
            summary = TextUtils.CHAR_SEQUENCE_CREATOR.createFromParcel(in);
            breadCrumbTitleRes = in.readInt();
            breadCrumbTitle = TextUtils.CHAR_SEQUENCE_CREATOR.createFromParcel(in);
            breadCrumbShortTitleRes = in.readInt();
            breadCrumbShortTitle = TextUtils.CHAR_SEQUENCE_CREATOR.createFromParcel(in);
            iconRes = in.readInt();
            fragment = in.readString();
            fragmentArguments = in.readBundle();
            if (in.readInt() != 0)  intent = Intent.CREATOR.createFromParcel(in);
            extras = in.readBundle();
        }
        Header(Parcel in) {
            readFromParcel(in);
        }
        public static final @android.annotation.NonNull Creator<Header> CREATOR = new Creator<Header>() {
            public Header createFromParcel(Parcel source) {
                return new Header(source);
            }
            public Header[] newArray(int size) {
                return new Header[size];
            }
        };
    }
    private static class HeaderAdapter extends ArrayAdapter<Header> {
        private static class HeaderViewHolder {
            ImageView icon;
            TextView title;
            TextView summary;
        }
        private LayoutInflater mInflater;
        private int mLayoutResId;
        private boolean mRemoveIconIfEmpty;
        public HeaderAdapter(Context context, List<Header> objects, int layoutResId, boolean removeIconBehavior) {
            super(context, 0, objects);
            mInflater = (LayoutInflater)context.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
            mLayoutResId = layoutResId;
            mRemoveIconIfEmpty = removeIconBehavior;
        }
        @Override
        public View getView(int position, View convertView, ViewGroup parent) {
            HeaderViewHolder holder;
            View view;
            if (convertView == null) {
                view = mInflater.inflate(mLayoutResId, parent, false);
                holder = new HeaderViewHolder();
                holder.icon = (ImageView) view.findViewById(com.android.internal.R.id.icon);
                holder.title = (TextView) view.findViewById(com.android.internal.R.id.title);
                holder.summary = (TextView) view.findViewById(com.android.internal.R.id.summary);
                view.setTag(holder);
            } else {
                view = convertView;
                holder = (HeaderViewHolder) view.getTag();
            }
        }
    }
}

1.3、切换fragment

//frameworks/base/core/java/android/preference/PreferenceActivity.java
@Deprecated public abstract class PreferenceActivity extends ListActivity{
    //该方法很多地方被调用,例如创建或者点击需要切换fragment或者当前header的时候
    public void switchToHeader(Header header) {
        //判断是否当前header,是不切换
        if (mCurHeader == header) {
            getFragmentManager().popBackStack(BACK_STACK_PREFS,  FragmentManager.POP_BACK_STACK_INCLUSIVE);
        } else {
            if (header.fragment == null)   throw new IllegalStateException("can't switch to header that has no fragment");
            //切换对应的fragment
            switchToHeaderInner(header.fragment, header.fragmentArguments);
            //切换header布局里面的字符串和图标等信息
            setSelectedHeader(header);
        }
    }
    //还是使用了FragmentTransaction 方式进行切换
    private void switchToHeaderInner(String fragmentName, Bundle args) {
        getFragmentManager().popBackStack(BACK_STACK_PREFS, FragmentManager.POP_BACK_STACK_INCLUSIVE);
        if (!isValidFragment(fragmentName))  throw new IllegalArgumentException("Invalid fragment for this activity: " + fragmentName); 
        Fragment f = Fragment.instantiate(this, fragmentName, args);
        FragmentTransaction transaction = getFragmentManager().beginTransaction();
        transaction.setTransition(mSinglePane
                ? FragmentTransaction.TRANSIT_NONE
                : FragmentTransaction.TRANSIT_FRAGMENT_FADE);
        transaction.replace(com.android.internal.R.id.prefs, f);
        transaction.commitAllowingStateLoss();
        if (mSinglePane && mPrefsContainer.getVisibility() == View.GONE) {
            mPrefsContainer.setVisibility(View.VISIBLE);
            mHeadersContainer.setVisibility(View.GONE);
        }
    }
    //切换header的内容,通过触发点击事件
    void setSelectedHeader(Header header) {
        mCurHeader = header;
        int index = mHeaders.indexOf(header);
        if (index >= 0)  getListView().setItemChecked(index, true);
        else  getListView().clearChoices();
        showBreadCrumbs(header);
    }
}

2、剖析PreferenceFragment

2.1、布局文件加载

PreferenceFragment的布局跟PreferenceActivity类似,如下代码:

//frameworks/base/core/java/android/preference/PreferenceFragment.java
@Deprecated public abstract class PreferenceFragment extends Fragment implements
        /*Preference树结构下所有视图控件点击回调事件*/
        PreferenceManager.OnPreferenceTreeClickListener {
    @UnsupportedAppUsage
    private PreferenceManager mPreferenceManager;
    private ListView mList;
    //PreferenceFragment的布局文件,与PreferenceActivity使用了同样的布局
    private int mLayoutResId = com.android.internal.R.layout.preference_list_fragment;
    private static final int MSG_BIND_PREFERENCES = 1;
    private Handler mHandler = new Handler() {
        @Override public void handleMessage(Message msg) {
            switch (msg.what) {
                case MSG_BIND_PREFERENCES:
                    bindPreferences();
                    break;
            }
        }
    };
    @Override
    public void onCreate(@Nullable Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        mPreferenceManager = new PreferenceManager(getActivity(), FIRST_REQUEST_CODE);
        mPreferenceManager.setFragment(this);
    }
    @Override
    public View onCreateView(LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
        //加载preference_list_fragment.xml,如果有acitivity沿用
        mLayoutResId = a.getResourceId(com.android.internal.R.styleable.PreferenceFragment_layout, mLayoutResId);
        return inflater.inflate(mLayoutResId, container, false);
    }
    //从布局文件中找到list
    @Override public void onViewCreated(View view, @Nullable Bundle savedInstanceState) {
        super.onViewCreated(view, savedInstanceState);
        ListView lv = (ListView) view.findViewById(android.R.id.list);
        if (lv != null && a.hasValueOrEmpty(com.android.internal.R.styleable.PreferenceFragment_divider)) {
            lv.setDivider( a.getDrawable(com.android.internal.R.styleable.PreferenceFragment_divider));
        }
    }
    //这里很重要
    @Override public void onActivityCreated(@Nullable Bundle savedInstanceState) {
        super.onActivityCreated(savedInstanceState);
        //bindPreferences方法是用来绑定加载所有的Preference类的
        if (mHavePrefs)  bindPreferences(); 
        mInitDone = true;
        if (savedInstanceState != null) {
            Bundle container = savedInstanceState.getBundle(PREFERENCES_TAG);
            if (container != null) {
                //如果有缓存PreferenceScreen,直接获取出来
                final PreferenceScreen preferenceScreen = getPreferenceScreen();
                if (preferenceScreen != null)   preferenceScreen.restoreHierarchyState(container); 
            }
        }
    }
    //设置Preference点击事件
    @Override public void onStart() {
        super.onStart();
        mPreferenceManager.setOnPreferenceTreeClickListener(this);
    }
    @Override public void onStop() {
        super.onStop();
        mPreferenceManager.dispatchActivityStop();
        mPreferenceManager.setOnPreferenceTreeClickListener(null);
    }
}

2.2、什么时候回调onBindPreferences方法?

public abstract class PreferenceFragment{
    private static final int MSG_BIND_PREFERENCES = 1;
    private Handler mHandler = new Handler() {
        @Override public void handleMessage(Message msg) {
            switch (msg.what) {
                case MSG_BIND_PREFERENCES:
                    bindPreferences();
                    break;
            }
        }
    };
    private void bindPreferences() {
        //获取当前对应的PreferenceScreen
        final PreferenceScreen preferenceScreen = getPreferenceScreen();
        if (preferenceScreen != null) {
            View root = getView();
            if (root != null) {
                View titleView = root.findViewById(android.R.id.title);
                if (titleView instanceof TextView) {
                    CharSequence title = preferenceScreen.getTitle();
                    if (TextUtils.isEmpty(title)) {
                        titleView.setVisibility(View.GONE);
                    } else {
                        ((TextView) titleView).setText(title);
                        titleView.setVisibility(View.VISIBLE);
                    }
                }
            }
            preferenceScreen.bind(getListView());
        }
        //回调子类(继承于PreferenceFragment中的onBindPreferences方法,通常在子类该方法钟回调所有需要显示的Preference控件)
        onBindPreferences();
    }
    //通过mPreferenceManager获取当前的PreferenceScreen 
    public PreferenceScreen getPreferenceScreen() {
        return mPreferenceManager.getPreferenceScreen();
    }
    //通过mPreferenceManager设置当前的PreferenceScreen 
    public void setPreferenceScreen(PreferenceScreen preferenceScreen) {
        if (mPreferenceManager.setPreferences(preferenceScreen) && preferenceScreen != null) {
            onUnbindPreferences();
            mHavePrefs = true;
            if (mInitDone)  postBindPreferences();
        }
    }
    //这个PreferenceScreen怎么来的呢,最终还是用户调用该接口设置进去的
    public void addPreferencesFromResource(@XmlRes int preferencesResId) {
        requirePreferenceManager();
        setPreferenceScreen(mPreferenceManager.inflateFromResource(getActivity(), preferencesResId, getPreferenceScreen()));
    }
}

Logo

开放原子开发者工作坊旨在鼓励更多人参与开源活动,与志同道合的开发者们相互交流开发经验、分享开发心得、获取前沿技术趋势。工作坊有多种形式的开发者活动,如meetup、训练营等,主打技术交流,干货满满,真诚地邀请各位开发者共同参与!

更多推荐