Android Settings总结

1.Settings简介

Settings,包括手机各项属性的基本调整和功能的开关,是用户根据个人喜好对手机进行定制的最方便的入口,也是用户在日常生活中使用频率最高的模块之一。因此,它的稳定性、修改定制,对于开发者来说尤为重要。

在目前的移动设备中,Settings界面除过主题定制的颜色图标等差别外,存在两种形式:单页形式和分页形式。单页形式为主要形式,而在平板等大屏设备中,则会采用分页形式。
单页
分页
图1 单页(左)和分页(右)

原生的Android4.0以后的系统中,将设置分为四个部分:

WIRELESS&NETWORKS:SIM卡管理,流量使用情况,飞行模式,VPN,网络共享等。
DEVICE:情景模式,显示,存储,电池,应用程序。
PERSONAL:账户与同步,位置服务,安全,语言和输入法,备份和重置。
SYSTEM:日期和时间,定时开关及,辅助功能,开发人员选项,关于手机。

2.Settings代码结构

Settings其实是以应用Settings.apk的形式存在于手机系统中的。在Google源码中的路径为:

/packages/apps/Settings/src

具体代码结构图如下:
Settings代码结构图
图2 Settings代码结构图

Settings第一级菜单的显示主要由包com.android.settings下面的Settings.java来负责控制。在该包下面,还包含了其他一些功能设置项的控制类,比如DisplaySettings.java等。其他包从包名基本可以看出,具体负责对应功能模块的控制。各个功能模块封装相对独立,这样,我们只需要进入具体模块,一般就可以完成对其的修改。

3.Settings配置文件

既然是APK,我们进入AndroidManifest.xml文件中可以看到它的配置信息。在该文件中,有相当多的权限使用声明,这正是因为Settings包含众多的模块,不同的模块可能需要不同的权限所致。

<application android:label="@string/settings_label"
            android:icon="@mipmap/ic_launcher_settings"
            android:taskAffinity=""
            android:theme="@style/Theme.Settings"
            android:hardwareAccelerated="true"
            android:requiredForAllUsers="true"
            android:supportsRtl="true">

        <!-- Settings -->

        <activity android:name="Settings"
                android:label="@string/settings_label_launcher"
                android:taskAffinity="com.android.settings"
                android:configChanges="keyboardHidden|screenSize|mcc|mnc"
                android:launchMode="singleTask">
            <intent-filter>
                <action android:name="android.intent.action.MAIN" />
                <action android:name="android.settings.SETTINGS" />
                <category android:name="android.intent.category.DEFAULT" />
                <category android:name="android.intent.category.LAUNCHER" />
                <category android:name="android.intent.category.APP_SETTINGS" />
            </intent-filter>
        </activity>

        <activity android:name=".SubSettings"
                android:taskAffinity="com.android.settings"
                android:configChanges="orientation|keyboardHidden|screenSize|mcc|mnc"
                android:parentActivityName="Settings">
        </activity>
……

第一个标签中,”android.intent.action.MAIN”的action配合”android.intent.category.DEFUALT”的category,决定了整个Settings.APK默认从Settings这个Activity进入。Settings在Launcher进入时,启动的是Settings.java,由”android.intent.category.LAUNCHER”决定。

而整个APK在Launcher中的图标,目标进程,主题,硬件加速,是否面向所有用户,是否支持阿拉伯语等属性在标签下进行定义。

在上面的代码最后,还有一个SubSettings的activity,这也是比较重要的一个类,在小分辨率(未分页)的时候,Settings绝大部分二级菜单都是在SubSettings这个activity中负责控制的。这个后面再讲。

4.Settings实现原理

Settings第一级菜单,是一个ListView,每一个item都是由一个Header构成,整个列表由HeaderAdapter来进行适配。在适配的时候,会取出Header的icon以及title,summary等并放入HeaderViewHolder中,这些就是我们在图一左中看到的外在信息。

然后是对各item的监听,当点击一个item的时候,跳转到具体的模块对应的Fragment中去。

分页模式和单页模式在基本实现上是一致的,区别在于分页模式Header和对应的Fragment将同时显示,因此,在对应模块的Fragment的显示的时候有区别,这个后面再讲。

以上,是Settings实现的基本流程,出现的几个词汇分别是Header、Fragment、HeaderAdapter、HeaderViewHolder,后面代码遇到的时候会讲。这里知道大概流程以及需要这些组件就可以了。

5.Settings代码分析

5.1 父类PreferenceActivity.java

我们首先进入Settings.java,它的注释说得很清楚,这个类是用来处理Settings单页和双页的UI布局的顶级Activity。

/**
 * Top-level settings activity to handle single pane and double pane UI layout.
 */
public class Settings extends PreferenceActivity
        implements ButtonBarHandler, OnAccountsUpdateListener {

它继承于PreferenceActivity,并实现了ButtonBarHandler和onAccountsUpdateListener接口。PreferencActivity以下简称PA,需要重点分析,因为在当前Settings.java中的部分方法就是重写PA的,有很多重要的代码,单单在Settings.java中是无法理解的,必须进入PA中,才能发现根本原理。而两个接口,只是为了增加按钮栏的处理和账户更新处理的功能,我们不去深入讲。

5.2 布局文件preference_content_list.xml

在PA的onCreate()方法中,通过setContentView()设置了preference_content_list的布局,该布局文件定义了Settings的主要界面表现形式。代码如下。

<LinearLayoutxmlns:android="http://schemas.android.com/apk/res/android"
    android:orientation="vertical"
    android:layout_height="match_parent"
    android:layout_width="match_parent">

    <LinearLayoutandroid:orientation="horizontal"
        android:layout_width="match_parent"
        android:layout_height="0px"
        android:layout_weight="1">

        <LinearLayoutstyle="?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> /③

        <LinearLayoutandroid: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"
                android:visibility="gone" >

            <!-- Breadcrumb inserted here, in certain screen sizes. In others, it will be an
                empty layout or just padding, and PreferenceActivity will put the breadcrumbs in
                the action bar. -->
            <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> /②

    <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="@string/back_button_label"
        />
        <LinearLayoutandroid: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="@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="@string/next_button_label"
            />
        </LinearLayout> /⑦
    </RelativeLayout> /⑥
</LinearLayout> /①

布局图如下:
PreferenceActivity布局图
图3 PreferenceActivity布局图

从上面布局图,很容易看出,id为headers的LinearLayout即放置HeaderList的地方,右侧则为放置Fragment的地方。单页的时候,只显示左侧LinearLayout③;分页后,右侧的LinearLayout⑤由默认的不显示变为显示,就成为了图1(右)分页后的效果。至于最下方的RelativeLayout⑥,为返回、跳过、前进按钮,默认是不显示的。

5.3分页相关代码块

5.3.1 判断是否是分页模式的方法onIsMultiPane()

在PA中,有个方法onIsMultiPane()来判断是否需要进行分页显示。代码如下,而它其实是通过读取系统属性preferences_prefer_dual_pane的值来判定的。该布尔值位于/frameworks/base/core/res/res/values/bools.xml中。

/**
     * Called to determine if the activity should run in multi-pane mode.
     * The default implementation returns true if the screen is large
     * enough.
     */
    public boolean onIsMultiPane() {
        boolean preferMultiPane = getResources().getBoolean(
                com.android.internal.R.bool.preferences_prefer_dual_pane);
        return preferMultiPane;
    }

5.3.2 是否是单页模式的标志mSinglePane

在PA中,有一个布尔值mSinglePane专门用来标识是否是单页还是分页显示。

private boolean mSinglePane;

它在onCreate()方法中获得具体值,如果HeaderList被隐藏了(意味着此时只会显示具体模块的内容)或者非多页模式,那么mSinglePane即为true,表示单页模式。在PA中,涉及到切换到双页模式的几处关键代码,都和这个值有关。下面接着看其他地方。

boolean hidingHeaders = onIsHidingHeaders();
        mSinglePane = hidingHeaders || !onIsMultiPane();

5.3.3 控制Fragment的显示

下面的代码仍然在onCreate()方法中,重点看else分支,这个分支即表示切换到分页模式,如果是分页模式且initialFragment为空,也就是暂时没有要显示的Fragment,则通过onGetInitialHeader()方法获取一个初始Header,然后通过switchHeader(h)方法将Header(此时为分页模式,在显示该Header的时候会同样会将整个HeaderList显示出来)和对应的Fragment显示出来。如果initialFragment本来就不为空,则通过switchHeader(initialFragment,initialArgument)方法将此Fragment显示出来。

 if (initialFragment != null && mSinglePane) {
                Log.d(TAG, "    Show a fragment from EXTRA_SHOW_FRAGMENT.");

                // If we are just showing a fragment, we want to run in
                // new fragment mode, but don't need to compute and show
                // the headers.
                switchToHeader(initialFragment, initialArguments);
                if (initialTitle != 0) {
                    CharSequence initialTitleStr = getText(initialTitle);
                    CharSequence initialShortTitleStr = initialShortTitle != 0
                            ? getText(initialShortTitle) : null;
                    showBreadCrumbs(initialTitleStr, initialShortTitleStr);
                }

            } else {
                // We need to try to build the headers.
                onBuildHeaders(mHeaders);

                // If there are headers, then at this point we need to show
                // them and, depending on the screen, we may also show in-line
                // the currently selected preference fragment.
                if (mHeaders.size() > 0) {
                    Log.d(TAG, "    Build headers successfully.");

                    if (!mSinglePane) {
                        if (initialFragment == null) {
                            Header h = onGetInitialHeader();
                            switchToHeader(h);
                        } else {
                            switchToHeader(initialFragment, initialArguments);
                        }
                    }
                }

在上面代码中,出现几个重要方法:switchToHeader(initialFragment, initialArguments)、showBreadCrumbs(initialTitleStr, initialShortTitleStr)、switchToHeader(h)。可以说,这几个方法决定了分页显示的最终结果。下面将代码贴出来。

/**
     * When in two-pane mode, switch the fragment pane to show the given
     * preference fragment.
     *
     * @param fragmentName The name of the fragment to display.
     * @param args Optional arguments to supply to the fragment.
     */
    public void switchToHeader(String fragmentName, Bundle args) {
        setSelectedHeader(null);
        switchToHeaderInner(fragmentName, args, 0);
    }

    /**
     * When in two-pane mode, switch to the fragment pane to show the given
     * preference fragment.
     *
     * @param header The new header to display.
     */
    public void switchToHeader(Header header) {
        if (mCurHeader == header) {
            // This is the header we are currently displaying.  Just make sure
            // to pop the stack up to its root state.
            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");
            }
            int direction = mHeaders.indexOf(header) - mHeaders.indexOf(mCurHeader);
            switchToHeaderInner(header.fragment, header.fragmentArguments, direction);
            setSelectedHeader(header);
        }
    }

可以看到,这两个为switchToHeader()的参数重载方法。它们最终,都调用了方法switchToHeaderInner(),这个方法中对即将要显示的Fragment进行了初始化,并通过FragmentTransaction的方式启动。

 private void switchToHeaderInner(String fragmentName, Bundle args, int direction) {
        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(FragmentTransaction.TRANSIT_FRAGMENT_FADE);
        transaction.replace(com.android.internal.R.id.prefs, f);
        transaction.commitAllowingStateLoss();
    }

而showBreadCrumbs()为两个参数重载方法。

void showBreadCrumbs(Header header) {
        if (header != null) {
            CharSequence title = header.getBreadCrumbTitle(getResources());
            if (title == null) title = header.getTitle(getResources());
            if (title == null) title = getTitle();
            showBreadCrumbs(title, header.getBreadCrumbShortTitle(getResources()));
        } else {
            showBreadCrumbs(getTitle(), null);
        }
    }

上面这个单参数方法,最终其实也是调用了它的另外一个重载方法。它的功能。。。

/**
     * Change the base title of the bread crumbs for the current preferences.
     * This will normally be called for you.  See
     * {@link android.app.FragmentBreadCrumbs} for more information.
     */
    public void showBreadCrumbs(CharSequence title, CharSequence shortTitle) {
        if (mFragmentBreadCrumbs == null) {
            View crumbs = findViewById(android.R.id.title);
            // For screens with a different kind of title, don't create breadcrumbs.
            try {
                mFragmentBreadCrumbs = (FragmentBreadCrumbs)crumbs;
            } catch (ClassCastException e) {
                setTitle(title);
                return;
            }
            if (mFragmentBreadCrumbs == null) {
                if (title != null) {
                    setTitle(title);
                }
                return;
            }
            if (mSinglePane) {
                mFragmentBreadCrumbs.setVisibility(View.GONE);
                // Hide the breadcrumb section completely for single-pane
                View bcSection = findViewById(com.android.internal.R.id.breadcrumb_section);
                if (bcSection != null) bcSection.setVisibility(View.GONE);
                setTitle(title);
            }
            mFragmentBreadCrumbs.setMaxVisible(2);
            mFragmentBreadCrumbs.setActivity(this);
        }
        if (mFragmentBreadCrumbs.getVisibility() != View.VISIBLE) {
            setTitle(title);
        } else {
            mFragmentBreadCrumbs.setTitle(title, shortTitle);
            mFragmentBreadCrumbs.setParentTitle(null, null, null);
        }
    }

5.3.4 控制Fragment在其他配置下的显示

重新回到PA的onCreate()方法中,继续向下看。

  // The default configuration is to only show the list view.  Adjust
        // visibility for other configurations.
        if (initialFragment != null && mSinglePane) {
            Log.d(TAG, "    Single pane, showing just a prefs fragment.");

            // Single pane, showing just a prefs fragment.
            findViewById(com.android.internal.R.id.headers).setVisibility(View.GONE);
            mPrefsContainer.setVisibility(View.VISIBLE);
            if (initialTitle != 0) {
                CharSequence initialTitleStr = getText(initialTitle);
                CharSequence initialShortTitleStr = initialShortTitle != 0
                        ? getText(initialShortTitle) : null;
                showBreadCrumbs(initialTitleStr, initialShortTitleStr);
            }
        } else if (mHeaders.size() > 0) {
            Log.d(TAG, "    Set list adapter created from headers.");

            setListAdapter(new HeaderAdapter(this, mHeaders));
            if (!mSinglePane) {
                // Multi-pane.
                getListView().setChoiceMode(AbsListView.CHOICE_MODE_SINGLE);
                if (mCurHeader != null) {
                    setSelectedHeader(mCurHeader);
                }
                mPrefsContainer.setVisibility(View.VISIBLE);
            }
        } else {
            Log.d(TAG, "    In the old \"just show a screen of preferences\" mode.");

            // If there are no headers, we are in the old "just show a screen
            // of preferences" mode.
            setContentView(com.android.internal.R.layout.preference_list_content_single);
            mListFooter = (FrameLayout) findViewById(com.android.internal.R.id.list_footer);
            mPrefsContainer = (ViewGroup) findViewById(com.android.internal.R.id.prefs);
            mPreferenceManager = new PreferenceManager(this, FIRST_REQUEST_CODE);
            mPreferenceManager.setOnPreferenceTreeClickListener(this);
        }

5.3.5 Settings.java中设置Settings的label

在Settings.java中,也有部分与分页有关的代码。这部分代码,主要是PreferenceActivity无法直接满足Settings具体的要求而进行修改定制时所用。下面这段代码在onCreate()方法中,作用是在分页模式下,将界面的标题设置为Settings的label。这样从Launcher一进入Settings第一级菜单,就会看到左上角的应用标题为Settings。没有这段代码,前面提到的在PA的onCreate()方法中的onGetInitialHeader()方法将会生效,那么第一次进入后将使用HeaderList的第一个Header(WifiSettings)的标题作为标题。

 if (!onIsHidingHeaders() && onIsMultiPane()) {
            highlightHeader(mTopLevelHeaderId);
            // Force the title so that it doesn't get overridden by a direct launch of
            // a specific settings screen.
            setTitle(R.string.settings_label);
        }

5.3.6 Settings中禁用顶端Home返回键

仍然在Settings的onCreate()方法中,下面的代码用于在分页的时候,禁用界面顶端的Home返回键。从这些代码看出,如果要对Settings的一级菜单进行定制,在onCreate()方法中增加相应的控制代码就可以。

  // Override up navigation for multi-pane, since we handle it in the fragment breadcrumbs
        if (onIsMultiPane()) {
            getActionBar().setDisplayHomeAsUpEnabled(false);
            getActionBar().setHomeButtonEnabled(false);
        }

5.3.7 Settings.java中的onNewIntent函数

如果不是从历史栈中启动,将重置到一级菜单。如果是分页模式,将调用switchToHeaderLocal()方法,其最终调用的是PA的switchToHeader()方法,前面已经有介绍。

   @Override
    public void onNewIntent(Intent intent) {
        super.onNewIntent(intent);

        // If it is not launched from history, then reset to top-level
        if ((intent.getFlags() & Intent.FLAG_ACTIVITY_LAUNCHED_FROM_HISTORY) == 0) {
            if (mFirstHeader != null && !onIsHidingHeaders() && onIsMultiPane()) {
                switchToHeaderLocal(mFirstHeader);
            }
            getListView().setSelectionFromTop(0, 0);
        }
    }

5.3.8 Settings.java中的getIntent函数

下面getIntent()方法,作用是对传递过来的Intent作一下判断和处理,增加Extra信息。主要需要理解这个方法:getStartFragmentClass()。它将得到的superIntent中的组件名与Settings类的进行比对,如果相同则返回null;如果不同,则返回类名,使其能够以Fragment的形式进行加载。不难发现,这个方法对分页模式不会有任何影响。

 @Override
    public Intent getIntent() {
        Intent superIntent = super.getIntent();
        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 && !onIsMultiPane()) {
            Intent modIntent = new Intent(superIntent);
            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, superIntent.getExtras());
            return modIntent;
        }
        return superIntent;
    }

5.4 Settings.java核心代码

5.4.1 Settings.java的onCreate函数

onCreate()方法在刚才已经有讲过,主要对PA进行进一步的定制,不再多说。

5.4.2 Settings.java的onResume函数

onResume()方法中,进行了多个BroadcastRecevier的注册。其中一个比较重要的地方,就是对【开发者选项】的监听器。在用户版本,默认【开发者选项】是被隐藏的。只有在第一级菜单先进入【关于手机】,然后连续按7次【Build Number】后,才能将其激活,从而在第一级菜单中显示出来。下面的代码就是这个监听器的创建和注册。

   mDevelopmentPreferencesListener = new SharedPreferences.OnSharedPreferenceChangeListener() {
            @Override
            public void onSharedPreferenceChanged(SharedPreferences sharedPreferences, String key) {
                invalidateHeaders();
            }
        };
        mDevelopmentPreferences.registerOnSharedPreferenceChangeListener(
                mDevelopmentPreferencesListener);

5.4.3 Settings.java的onPause函数

在onPause()方法中,对在onResume()方法中注册的监听器进行unRegisterRecevier()操作。(代码略)

5.4.4 Settings.java的onBuilderHeaders函数

Settings一级菜单中几乎所有(账户相关的由代码中的具体方法控制增删,后面有讲)的Header均是通过onBuildHeaders()方法进行加载的。

  /**
     * Populate the activity with the top-level headers.
     */
    @Override
    public void onBuildHeaders(List<Header> headers) {
        if (!onIsHidingHeaders()) {
            PDebug.Start("loadHeadersFromResource");
            loadHeadersFromResource(R.xml.settings_headers, headers);
            PDebug.End("loadHeadersFromResource");
            updateHeaderList(headers);
        }
    }

从上面代码中,可以看到一个非常重要的XML文件:settings_headers.xml。所有要显示的Header均在这个文件中以 < header>的形式进行定义。

每一个< header>中,定义了id、icon、fragment、title属性,各自的作用分别为:id用来标识是哪个header;icon即Settings一级菜单上显示的每一个item前的图像;fragment用来指定具体启动的类,用完整的包名表示;title即一级菜单中每一个item的名称,例如Wifi、Bluetooth等。

所以,我们要增加一个功能的话,只需要在这个文件中增加一个< header>,然后实现对应的功能类即可。

<preference-headers
        xmlns:android="http://schemas.android.com/apk/res/android">


    <!-- WIRELESS and NETWORKS -->
    <header android:id="@+id/wireless_section"
        android:title="@string/header_category_wireless_networks" />

    <!-- Sim management -->
    <header
        android:id="@+id/sim_settings"
        android:icon="@drawable/ic_settings_dualsim"
        android:fragment="com.mediatek.gemini.SimManagement"
        android:title="@string/gemini_sim_management_title" />
    <!-- Wifi -->
    <header
        android:id="@+id/wifi_settings"
        android:fragment="com.android.settings.wifi.WifiSettings"
        android:title="@string/wifi_settings_title"
        android:icon="@drawable/ic_settings_wireless" />

    <!-- Bluetooth -->
    <header
        android:id="@+id/bluetooth_settings"
        android:fragment="com.android.settings.bluetooth.BluetoothSettings"
        android:title="@string/bluetooth_settings_title"
        android:icon="@drawable/ic_settings_bluetooth2" />

 ……(省略中间部分)……

    <header
        android:id="@+id/about_settings"
        android:fragment="com.android.settings.DeviceInfoSettings"
        android:icon="@drawable/ic_settings_about"
        android:title="@string/about_settings" />

</preference-headers>

5.4.5 Settings.java的updateHeaderList函数

接着上面,在通过loadHeadersFromResource()方法加载settings_headers.xml后,紧跟着调用方法updateHeaderList(headers)对headers做具体的处理。这块的代码比较长,具体所做的事就是对各个具体功能根据需要进行控制,代码逻辑非常清晰,就不再贴出来了。

5.4.6 Settings.java的HeaderAdapter内部类

HeaderAdapter为整个Settings的一级菜单ListView的适配器,它声明为了Settings.java的静态内部类,继承自ArrayAdapter。与所有Adapter一样,它的主要内容是将我们之前得到的headersList如何显示在ListView中去。我讲一下需要特别理解的主要思路:如下面代码,预先定义了5种Header的类型,以满足对不同外观的Header的分别管控处理,比如Wifi和蓝牙这样的带有开关的,即HEADER_TYPE_SWITCH;DisplaySettings这样正常的,即HEADER_TYPE_NORMAL;快速启动这样带有CheckBox的,即HEADER_TYPE_CHECK;还有fragment和intent都为空的只是为了做区分的Header,即HEADER_TYPE_CHECK。

不同的Header可以自定义不同的XML布局,这样,就使得一级菜单每一个item根据功能的不同表现出不同的外观。

   private static class HeaderAdapter extends ArrayAdapter<Header> implements
            CompoundButton.OnCheckedChangeListener {
        static final int HEADER_TYPE_CATEGORY = 0;
        static final int HEADER_TYPE_NORMAL = 1;
        static final int HEADER_TYPE_SWITCH = 2;
        static final int HEADER_TYPE_BUTTON = 3;
        static final int HEADER_TYPE_CHECK = 4;
        private static final int HEADER_TYPE_COUNT = HEADER_TYPE_CHECK + 1;

在HeaderAdapter.java中,又定义了一个内部静态类HeaderViewHolder,它相当每一个Header在ListView中要表现的所有元素的数据类型集合。

      private static class HeaderViewHolder {
            ImageView icon;
            TextView title;
            TextView summary;
            Switch switch_;
            CheckBox check;//add by yangzhong.gong for FR581773
            ImageButton button_;
            View divider_;
        }

而获取视图的方法为getView(),主要思路很简单,就是判断不同类型的HEADER_TYPE,然后根据不同的HEADER_TYPE做不同的处理,将所需要的title、icon等等信息装入事先定义好的HeaderViewHolder对象。然后用setTag(holder)方法传递给view对象,最后将view返回。(代码略)

最后就是单击等操作的监听器设置。这里只是想强调一点,附加功能都可以通过接口来实现,例如在上个例子里,就实现了CompoundButton.OnCheckedChangeListener接口,而这个是我们在做定制时自己添加的。

@Override
    public void onHeaderClick(Header header, int position) {
        boolean revert = false;
        if (header.id == R.id.account_add) {
            revert = true;
        }

        super.onHeaderClick(header, position);

        if (revert && mLastHeader != null) {
            highlightHeader((int) mLastHeader.id);
        } else {
            mLastHeader = header;
        }

        if (header.id == R.id.regulatory_safety) {
            Intent intent = new Intent();
            ComponentName comp = new ComponentName(
                    "com.eyelike.Elabel",
                    "com.eyelike.Elabel.SettingsRegulatoryActivity");
            intent.setComponent(comp);
            startActivity(intent);
        }
    }

5.5 定制相关的代码块

5.5.1 定制fragment

在Settings.java中定义了一个字符串数组ENTY_FRAGMENTS,这个数组的声明与方法isValidFragment()关系甚大。而isValidFragment()方法是PA用来判断Fragment是否可用的,在Settings.java中做了复写。

之前在讲Settings主要实现原理的时候有讲,每一个具体功能都由Fragment来实现。如果我们想要在第一级菜单中增加一个功能,只需要在

private static final String[] ENTRY_FRAGMENTS = {
        WirelessSettings.class.getName(),
        WifiSettings.class.getName(),
        AdvancedWifiSettings.class.getName(),
        BluetoothSettings.class.getName(),
        TetherSettings.class.getName(),
        WifiP2pSettings.class.getName(),
        VpnSettings.class.getName(),
        DateTimeSettings.class.getName(),
        LocalePicker.class.getName(),
        InputMethodAndLanguageSettings.class.getName(),
        SpellCheckersSettings.class.getName(),
        UserDictionaryList.class.getName(),
        UserDictionarySettings.class.getName(),
        SoundSettings.class.getName(),
        DisplaySettings.class.getName(),
        DeviceInfoSettings.class.getName(),
        ManageApplications.class.getName(),
        ProcessStatsUi.class.getName(),
        NotificationStation.class.getName(),
        LocationSettings.class.getName(),
        SecuritySettings.class.getName(),
        PrivacySettings.class.getName(),
        DeviceAdminSettings.class.getName(),
        AccessibilitySettings.class.getName(),
        ToggleCaptioningPreferenceFragment.class.getName(),
        TextToSpeechSettings.class.getName(),
        Memory.class.getName(),
        DevelopmentSettings.class.getName(),
        UsbSettings.class.getName(),
        AndroidBeam.class.getName(),
        WifiDisplaySettings.class.getName(),
        PowerUsageSummary.class.getName(),
        AccountSyncSettings.class.getName(),
        CryptKeeperSettings.class.getName(),
        DataUsageSummary.class.getName(),
        DreamSettings.class.getName(),
        UserSettings.class.getName(),
        NotificationAccessSettings.class.getName(),
        ManageAccountsSettings.class.getName(),
        PrintSettingsFragment.class.getName(),
        PrintJobSettingsFragment.class.getName(),
        TrustedCredentialsSettings.class.getName(),
        PaymentSettings.class.getName(),
        KeyboardLayoutPickerFragment.class.getName(),
        //M@:
        SimManagement.class.getName(),
        SimInfoEditor.class.getName(),
        //Class name same as Activity name so use full name here
        com.mediatek.gemini.SimDataRoamingSettings.class.getName(),
        AudioProfileSettings.class.getName(),
        Editprofile.class.getName(),
        HDMISettings.class.getName(),
        SelectSimCardFragment.class.getName(),
        UsbSharingChoose.class.getName(),
        UsbSharingInfo.class.getName(),
        TetherWifiSettings.class.getName(),
        DrmSettings.class.getName(),
        NfcSettings.class.getName(),
        WifiGprsSelector.class.getName(),
        BeamShareHistory.class.getName(),
        CardEmulationSettings.class.getName(),
        MtkAndroidBeam.class.getName(),
        HotKnotSettings.class.getName(),
        MasterClear.class.getName()//add by eyelike
    };

    @Override
    protected boolean isValidFragment(String fragmentName) {
        // Almost all fragments are wrapped in this,
        // except for a few that have their own activities.
        for (int i = 0; i < ENTRY_FRAGMENTS.length; i++) {
            if (ENTRY_FRAGMENTS[i].equals(fragmentName)) return true;
        }
        return false;
    }

5.5.2 定制ActionBar

在Settings的第二级菜单,也就是各个具体功能界面,大量应用了ActionBar(界面最上方的条状栏,右侧往往有开关等功能按钮)。而在Wifi、蓝牙等设置界面,有时候会看到界面下方也有按钮,如下图。
这里写图片描述
图4 ActionBar分离示例图
这是由ActionBar的属性来控制的,对应于XML文件中的属性为:

android:uiOptions="splitActionBarWhenNarrow"

而在Settings.java中的控制在onBuildStartFragmentIntent()方法中,代码如下。如果要修改相关功能,只需在其中做增删即可。

 @Override
    public Intent onBuildStartFragmentIntent(String fragmentName, Bundle args,
            int titleRes, int shortTitleRes) {
        Intent intent = super.onBuildStartFragmentIntent(fragmentName, args,
                titleRes, shortTitleRes);

        // Some fragments want split ActionBar; these should stay in sync with
        // uiOptions for fragments also defined as activities in manifest.
        if (WifiSettings.class.getName().equals(fragmentName) ||
                WifiP2pSettings.class.getName().equals(fragmentName) ||
                BluetoothSettings.class.getName().equals(fragmentName) ||
                DreamSettings.class.getName().equals(fragmentName) ||
                LocationSettings.class.getName().equals(fragmentName) ||
                BeamShareHistory.class.getName().equals(fragmentName) ||
                MtkAndroidBeam.class.getName().equals(fragmentName) ||
                AudioProfileSettings.class.getName().equals(fragmentName) ||                
                ToggleAccessibilityServicePreferenceFragment.class.getName().equals(fragmentName) ||
                PrintSettingsFragment.class.getName().equals(fragmentName) ||
                PrintServiceSettingsFragment.class.getName().equals(fragmentName) ||
                HotKnotSettings.class.getName().equals(fragmentName)) {
            intent.putExtra(EXTRA_UI_OPTIONS, ActivityInfo.UIOPTION_SPLIT_ACTION_BAR_WHEN_NARROW);
        }

        intent.setClass(this, SubSettings.class);
        return intent;
    }

5.6 Settings其他重要问题释疑

以上通过代码段对主要实现进行了介绍,但是,如果跳出一小块一小块代码,从整体上来看,还是会有一些一时难以琢磨理解的疑问。下面,就将我曾经遇到的一些主要疑问列出来,并做一些解答。

5.6.1 为什么使用Hierarchyviewer 工具查看时Settings中的很多界面显示的都是SubSettings?

要解决这个问题我们先要清楚为什么会写一个SubSettings.java继承自Settings.java?SubSettings.java的内容非常简单,代码如下。

/**
 * Stub class for showing sub-settings; we can't use the main Settings class
 * since for our app it is a special singleTask class.
 */
public class SubSettings extends Settings {

    @Override
    public boolean onNavigateUp() {
        finish();
        return true;
    }

    @Override
    protected boolean isValidFragment(String fragmentName) {
        Log.d("SubSettings", "Launching fragment " + fragmentName);
        return true;
    }
}

SubSettings.java中的注释很清楚的告诉了我们原因:Stub class for showing sub-settings; we can’t use the main Settings class since for our app it is a special singleTask class。

原来是因为Settings.java在声明时指定了android:launchMode=”singleTask”。

要显示Fragment的内容,我们就必须为其指定一个Activity。而Settings中的很多设置界面是由PreferenceFragment来完成的,当然也需要我们指定Activity。PA中得onBuildStartFragmentIntent函数会为我们构造一个显示Fragment的Intent对象(该函数的注释写的非常明白)。Settings.java重写了这个函数(见4.2,重写时它调用了super的该方法),在为intent对象setClass时都使用SubSettings.java(注:在settings_headers.xml指定了intent的header是不会触发onBuildStartFragmentIntent的)。

结果就是,Settings中大部分fragment都是使用的SubSettings这个Activity来显示。由于Hierarchyviewer只是显示当前界面使用的Activity(不能显示这个界面是由哪个Fragment构造的),所以我们使用Hierarchyviewer 对Settings进行观察时很多设置界面显示的是SubSettings。

5.6.2 Hierarchyviewer 中显示SubSetting时如何确定我进入的是哪个fragment?

在res/xml/settings_headers.xml中声明了各个header被点击后使用的fragment。我们可以根据这个文件确定我们进入的fragment。

例如,当我们点击Display时Hierarchyviewer 中显示SubSetting。我们通过查找settings_headers就可知道使用的是哪个fragment(见5.1)。header中使用 android:fragment指明使用的fragment。由此可知,Display使用的是com.android.settings.DisplaySettings这个fragment。

5.6.3 点击设置界面的某一个header时,设置界面是如何切换的?

点击设置界面的header时,会触发Settings中onHeaderClick函数,主要的处理都在其父类PreferenceActivity的onHeaderClick中实现的。如果这个header指定了fragment,在mSinglePane(见5.3)为true时,会调用startWithFragment方法,在startWithFragment方法中将调用onBuildStartFragmentIntent方法来构造intent对象(重要),最后使用该intent对象启动一个activity来显示fragment。

以点击Settings中的Display为例(Bluetooth同理,只不过启动的Activity变为BluetoothSettingsActivity(继承自Settings,但是没有实现重写任何方法,所以与SubSettings是一样的处理),fragment变为 com.android.settings.bluetooth.BluetoothSettings)。fragment是com.android.settings.DisplaySettings,activity是com.android.settings.SubSettings(fragment是由onHeaderClick函数传入的,activity是由onBuildStartFragmentIntent()指定的)。

执行startActivity后将启动SubSettings.java。即我们将会再一次执行SubSettings和PreferenceActivity的onCreate方法(因为Settings.java的onCreate方法调用了super.onCreate()),但是这次并不会进入Settings的主界面,因为我们的使用的intent对象是有很大不同的。这一次onCreate函数(PreferenceActivity)中的initialFragment 将被初始化为com.android.settings.DisplaySettings,然后我们将进入switchToHeader(),最后switchToHeaderInner会取得FragmentTransaction对象(见5.6),然后执行了transaction.replace(com.android.internal.R.id.prefs, f)。就这样把我们的fragment显示出来了。在onCreate中会对其他view的visibility进行设置,以保证只显示prefs。如,将com.android.internal.R.id.headers的visibility设置为VIEW.GONE。

5.6.4 Settings.java中getMetaData与getStartingFragmentClass这两个函数是否有点矛盾?

这两个函数可以说是相辅相成的。getMetaData会从AndroidManifest.xml中读取Activity的节点的数据;getStartingFragmentClass则从启动Activity的intent中读取数据。这两个函数会对读取到的数据进行整合,getStartingFragmentClass依赖于getMetaData读取到的数据,但是它也可能对数据作出修改(为了兼容性,如对原有manage apps类进行特殊处理)。

5.6.5 Settings的shortcut是如何创建的?从shortcut进入Settings的流程是什么?

创建Settings的shortcut时Luancher将会启动CreateShortcut,创建shortcut所需的intent对象将会由CreateShortcut和其父类LuancherActivity共同构建(详见 CreateShortcut的onListItemClick),这时创建的Intent对象使用的就不是SubSettings了(LuancherActivity中intentForPosition函数执行setClassName()时使用的参数并不是SubSettings)。

 public Intent intentForPosition(int position) {
            if (mActivitiesList == null) {
                return null;
            }

            Intent intent = new Intent(mIntent);
            ListItem item = mActivitiesList.get(position);
            intent.setClassName(item.packageName, item.className);
            if (item.extras != null) {
                intent.putExtras(item.extras);
            }
            return intent;
        }

CreateShortcut中列出了可以创建shortcut的设置项,这些设置项怎样检索出来的?
原来,在创建LuancherActivity的ActivityAdapter对象时,其构造函数中执行了makeListItems函数,该函数将使用PackageManager的queryIntentActivities来根据intent对象查询符合条件的activity。使用的intent是从getTargetIntent函数返回的。不难发现,要想在CreateShortcut中显示,Activity在必须要有

<category android:name="com.android.settings.SHORTCUT" />

如果我们想将Security设置项添加到shortcut列表,我们只需要在androidmanifest.xml中声明Settings$SecuritySettingsActivity部分加上

<category android:name="com.android.settings.SHORTCUT" />

即可。

回到正题,点击shortcut进入Settings时,传入的Intent对象中包含了目标fragment和目标activity以及其他信息。PreferenceActivity得到了足够多的信息,因此在onCreate中将依次执行switchToHeader()->setSelectedHeader(null)->switchToHeaderInner()->transaction.replace(com.android.internal.R.id.prefs, f);
这样就完成了fragment的显示(使用的activity是从intent解析出来的。在switchToHeaderInner中执行Fragment.instantiate时使用的Context是this!!)。不像执行onHeaderClick那样会执行函数onBuildStartFragmentIntent(Settings中重写了该函数)来重新指定我们使用的Activity。

5.6.6 为什么我从Settings的shortcut进入时,Hierarchyviewer显示的就不是SubSettings(如Data usage)?

Hierarchyviewer中显示SubSettings是因为我们在onBuildStartFragmentIntent方法中做了特殊处理(详见问题二)。从shortcut进入Settings时不显示SubSettings是因为没有走这个函数,因此就不会显示为SubSettings了(详见问题六)。

5.6.7 Settings.java中很多继承自它的内部类都是空实现,为什么要写这些类?

空实现,使得他们虽然被声明,但仍然都将使用Settings.java中的函数(注意private的属性和方法的访问权限问题)。因此,这样的构造必定是为了其他的便利。注释讲了一点原因:声明的这些类都将作为Settings的子类,为的是在启动的时候保持独立性。这样能够提高各个设置项、整个Settings的灵活性,方便开发者进行扩展。

/*
     * Settings subclasses for launching independently.
     */

除此之外,和整个Settings的设计结构也由一定关系:

①这样声明非常清晰明朗,易于维护;
②可以让我们为单独的设置项添加 shortcut(如data usage),因为创建shortcut使用queryIntentActivities查询使用的activity;
③允许其它程序访问单独的设置项;
④结构设计需要,启动Activity会读取meta-data信息;
⑤使得某些设置项可以不使用SubSettings的属性。如,在Settings中点击Bluetooth时使用BluetoothSettingsActivity,启动Bluetooth时将使用BluetoothSettingsActivity的属性,如 android:clearTaskOnLaunch=”true”。
等等。

  • eyelike@2014-06-11
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值