Итак, решил написать небольшой пост(я не умелец в этом, поэтому — много кода, мало слов) о том, как сделать настройки как в официальном приложении Настройки в Android 4 (может и в 3.0 тоже). Наша цель:
0. Умение читать и понимать код без объяснений
1. Использование фрагментов
2. Использование header'ов
3. Разделение пунктов на категории
4. Поддержка всех разрешений экрана
5. Использовать SDK14
Фрагменты
Я предполагаю, что вы уже ознакомлены с таким понятием, как фрагменты в Android, иначе — пример простейшего фрагмента с настройками из xml-файла
public class TestFragment extends PreferenceFragment {
private CheckBoxPreference mTimeTextMode;
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
addPreferencesFromResource(R.xml.test_settings); //По аналогии с ресурсами для PreferenceActivity
}
}
Главный экран настроек: XML (preference-headers)
Пример xml-файла (ресурсов) наших настроек:
<?xml version="1.0" encoding="utf-8"?>
<preference-headers xmlns:android="http://schemas.android.com/apk/res/android" >
<!-- Категория --->
<header android:title="Важное" />
<!-- Пункт --->
<header
<!-- Путь к нашему фрагменту настроек --->
android:fragment="com.achep.example.TestFragment"
<!-- Иконка --->
android:icon="@drawable/ic_settings_test"
<!-- Текст --->
android:title="Пункт 1" />
<!-- Категория --->
<header android:title="Привет, хабрахабр!" />
<!-- Пункт --->
<header
android:fragment="com.achep.example.TestFragment"
android:icon="@drawable/ic_settings_test"
android:title="Пункт 2" />
<!-- Пункт --->
<header
android:fragment="com.achep.example.TestFragment"
android:icon="@drawable/ic_settings_test"
android:title="Пункт 3" />
</preference-headers>
Как Вы уже могли заметить, Категория от Пункт не отличается ничем, кроме дополнительного содержимого (в данном случае — использование фрагментов), что мы, конечно, и будем использовать впредь.
Главный экран настроек: Java (со всеми нашими категориями/пунктами)
Исходный код я «подсмотрел» у Google на гитхабе и немного модифицировал под свое приложение. Самая главная (и немного запутанная) часть — это написание своего Adapter'а с категориями, блекджеками и, конечно, всевозможными другими типами пунктов (CheckBox, Switch и другие Ваши собственные).
Settings.class
public class Settings extends PreferenceActivity {
private static final String LOG_TAG = "Settings";
private static final String META_DATA_KEY_HEADER_ID = "com.achep.example.settings.TOP_LEVEL_HEADER_ID";
private static final String META_DATA_KEY_FRAGMENT_CLASS = "com.achep.example.settings.FRAGMENT_CLASS";
private static final String META_DATA_KEY_PARENT_TITLE = "com.achep.stopwatch.PARENT_FRAGMENT_TITLE";
private static final String META_DATA_KEY_PARENT_FRAGMENT_CLASS = "com.achep.example.settings.PARENT_FRAGMENT_CLASS";
private static final String SAVE_KEY_CURRENT_HEADER = "com.achep.example.settings.CURRENT_HEADER";
private static final String SAVE_KEY_PARENT_HEADER = "com.achep.example.settings.PARENT_HEADER";
private String mFragmentClass;
private int mTopLevelHeaderId;
private Header mFirstHeader;
private Header mCurrentHeader;
private Header mParentHeader;
private boolean mInLocalHeaderSwitch;
protected HashMap<Integer, Integer> mHeaderIndexMap = new HashMap<Integer, Integer>();
private List<Header> mHeaders;
@Override
protected void onCreate(Bundle savedInstanceState) {
getMetaData();
mInLocalHeaderSwitch = true;
super.onCreate(savedInstanceState);
mInLocalHeaderSwitch = false;
if (!onIsHidingHeaders() && onIsMultiPane()) {
highlightHeader(mTopLevelHeaderId);
}
// Восстанавливаем сохраненные данные, если они есть
if (savedInstanceState != null) {
mCurrentHeader = savedInstanceState
.getParcelable(SAVE_KEY_CURRENT_HEADER);
mParentHeader = savedInstanceState
.getParcelable(SAVE_KEY_PARENT_HEADER);
}
//Если текущий header был сохранен - переместимся к нему
if (savedInstanceState != null && mCurrentHeader != null) {
showBreadCrumbs(mCurrentHeader.title, null);
}
if (mParentHeader != null) {
setParentTitle(mParentHeader.title, null, new OnClickListener() {
public void onClick(View v) {
switchToParent(mParentHeader.fragment);
}
});
}
// Override up navigation for multi-pane, since we handle it in the
// fragment breadcrumbs
if (onIsMultiPane()) {
getActionBar().setDisplayHomeAsUpEnabled(false);
getActionBar().setHomeButtonEnabled(false);
}
}
@Override
protected void onSaveInstanceState(Bundle outState) {
super.onSaveInstanceState(outState);
// Save the current fragment, if it is the same as originally launched
if (mCurrentHeader != null) {
outState.putParcelable(SAVE_KEY_CURRENT_HEADER, mCurrentHeader);
}
if (mParentHeader != null) {
outState.putParcelable(SAVE_KEY_PARENT_HEADER, mParentHeader);
}
}
@Override
public void onResume() {
super.onResume();
ListAdapter listAdapter = getListAdapter();
if (listAdapter instanceof HeaderAdapter) {
((HeaderAdapter) listAdapter).resume();
}
invalidateHeaders();
}
@Override
public void onPause() {
super.onPause();
ListAdapter listAdapter = getListAdapter();
if (listAdapter instanceof HeaderAdapter) {
((HeaderAdapter) listAdapter).pause();
}
}
private void switchToHeaderLocal(Header header) {
mInLocalHeaderSwitch = true;
switchToHeader(header);
mInLocalHeaderSwitch = false;
}
@Override
public void switchToHeader(Header header) {
if (!mInLocalHeaderSwitch) {
mCurrentHeader = null;
mParentHeader = null;
}
super.switchToHeader(header);
}
/**
* Switch to parent fragment and store the grand parent's info
*
* @param className
* name of the activity wrapper for the parent fragment.
*/
private void switchToParent(String className) {
final ComponentName cn = new ComponentName(this, className);
try {
final PackageManager pm = getPackageManager();
final ActivityInfo parentInfo = pm.getActivityInfo(cn,
PackageManager.GET_META_DATA);
if (parentInfo != null && parentInfo.metaData != null) {
String fragmentClass = parentInfo.metaData
.getString(META_DATA_KEY_FRAGMENT_CLASS);
CharSequence fragmentTitle = parentInfo.loadLabel(pm);
Header parentHeader = new Header();
parentHeader.fragment = fragmentClass;
parentHeader.title = fragmentTitle;
mCurrentHeader = parentHeader;
switchToHeaderLocal(parentHeader);
highlightHeader(mTopLevelHeaderId);
mParentHeader = new Header();
mParentHeader.fragment = parentInfo.metaData
.getString(META_DATA_KEY_PARENT_FRAGMENT_CLASS);
mParentHeader.title = parentInfo.metaData
.getString(META_DATA_KEY_PARENT_TITLE);
}
} catch (NameNotFoundException nnfe) {
Log.w(LOG_TAG, "Could not find parent activity : " + className);
}
}
@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
&& mFirstHeader != null
&& !onIsHidingHeaders()
&& onIsMultiPane()) {
switchToHeaderLocal(mFirstHeader);
}
}
private void highlightHeader(int id) {
if (id != 0) {
Integer index = mHeaderIndexMap.get(id);
if (index != null) {
getListView().setItemChecked(index, true);
getListView().smoothScrollToPosition(index);
}
}
}
@Override
public Intent getIntent() {
Intent superIntent = super.getIntent();
String startingFragment = getStartingFragmentClass(superIntent);
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;
}
/**
* Checks if the component name in the intent is different from the Settings
* class and returns the class name to load as a fragment.
*/
protected String getStartingFragmentClass(Intent intent) {
if (mFragmentClass != null)
return mFragmentClass;
String intentClass = intent.getComponent().getClassName();
if (intentClass.equals(getClass().getName()))
return null;
return intentClass;
}
/**
* Override initial header when an activity-alias is causing Settings to be
* launched for a specific fragment encoded in the android:name parameter.
*/
@Override
public Header onGetInitialHeader() {
String fragmentClass = getStartingFragmentClass(super.getIntent());
if (fragmentClass != null) {
Header header = new Header();
header.fragment = fragmentClass;
header.title = getTitle();
header.fragmentArguments = getIntent().getExtras();
mCurrentHeader = header;
return header;
}
return mFirstHeader;
}
@Override
public Intent onBuildStartFragmentIntent(String fragmentName, Bundle args,
int titleRes, int shortTitleRes) {
Intent intent = super.onBuildStartFragmentIntent(fragmentName, args,
titleRes, shortTitleRes);
intent.setClass(this, SubSettings.class);
return intent;
}
/**
* Populate the activity with the top-level headers.
*/
@Override
public void onBuildHeaders(List<Header> headers) {
loadHeadersFromResource(R.xml.preference_headers, headers);
mHeaders = headers;
}
private void getMetaData() {
try {
ActivityInfo ai = getPackageManager().getActivityInfo(
getComponentName(), PackageManager.GET_META_DATA);
if (ai == null || ai.metaData == null)
return;
mTopLevelHeaderId = ai.metaData.getInt(META_DATA_KEY_HEADER_ID);
mFragmentClass = ai.metaData
.getString(META_DATA_KEY_FRAGMENT_CLASS);
// Check if it has a parent specified and create a Header object
final int parentHeaderTitleRes = ai.metaData
.getInt(META_DATA_KEY_PARENT_TITLE);
String parentFragmentClass = ai.metaData
.getString(META_DATA_KEY_PARENT_FRAGMENT_CLASS);
if (parentFragmentClass != null) {
mParentHeader = new Header();
mParentHeader.fragment = parentFragmentClass;
if (parentHeaderTitleRes != 0) {
mParentHeader.title = getResources().getString(
parentHeaderTitleRes);
}
}
} catch (NameNotFoundException nnfe) {
// No recovery
}
}
private static class HeaderAdapter extends ArrayAdapter<Header> {
static final int HEADER_TYPE_CATEGORY = 0;
static final int HEADER_TYPE_NORMAL = 1;
private static final int HEADER_TYPE_COUNT = HEADER_TYPE_NORMAL + 1;
private final CheckBoxHeaderPreference mTimerSoundEnabler,
mTimerVibroEnabler;
private static class HeaderViewHolder {
ImageView icon;
TextView title;
}
private LayoutInflater mInflater;
static int getHeaderType(Header header) {
// Определяем тип нашего пункта по его параметрам.
// Конечно можно использовать ID'ы для более сложных систем
return header.fragment == null ? HEADER_TYPE_CATEGORY : HEADER_TYPE_NORMAL;
}
@Override
public int getItemViewType(int position) {
Header header = getItem(position);
return getHeaderType(header);
}
@Override
public boolean areAllItemsEnabled() {
return false; // because of categories
}
@Override
public boolean isEnabled(int position) {
return getItemViewType(position) != HEADER_TYPE_CATEGORY;
}
@Override
public int getViewTypeCount() {
return HEADER_TYPE_COUNT;
}
@Override
public boolean hasStableIds() {
return true;
}
public HeaderAdapter(Context context, List<Header> objects) {
super(context, 0, objects);
mInflater = (LayoutInflater) context
.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
}
@Override
public View getView(int position, View convertView, ViewGroup parent) {
HeaderViewHolder holder;
Header header = getItem(position);
int headerType = getHeaderType(header);
View view = null;
if (convertView == null) {
holder = new HeaderViewHolder();
switch (headerType) {
case HEADER_TYPE_CATEGORY:
view = new TextView(getContext(), null,
android.R.attr.listSeparatorTextViewStyle); // Выбираем стиль "Категория"
holder.title = (TextView) view;
break;
case HEADER_TYPE_NORMAL:
// Я использую свой layout для "обычных" пунктов
view = mInflater.inflate(R.layout.preference_header_item,
parent, false);
holder.icon = (ImageView) view
.findViewById(android.R.id.icon); // Добавляем иконку
holder.title = (TextView) view
.findViewById(android.R.id.title); // Добавляем текст
break;
}
view.setTag(holder);
} else {
view = convertView;
holder = (HeaderViewHolder) view.getTag();
}
// All view fields must be updated every time, because the view may
// be recycled
switch (headerType) {
case HEADER_TYPE_CATEGORY:
holder.title.setText(header.getTitle(getContext()
.getResources()));
break;
case HEADER_TYPE_NORMAL:
holder.icon.setImageResource(header.iconRes);
holder.title.setText(header.getTitle(getContext()
.getResources()));
break;
}
return view;
}
public void resume() {
// Для данного примера - ничего не делаем :)
}
public void pause() {
// Для данного примера - ничего не делаем :)
}
}
@Override
public boolean onPreferenceStartFragment(PreferenceFragment caller,
Preference pref) {
int titleRes = pref.getTitleRes();
startPreferencePanel(pref.getFragment(), pref.getExtras(), titleRes,
null, null, 0);
return true;
}
@Override
public void setListAdapter(ListAdapter adapter) {
if (mHeaders == null) {
mHeaders = new ArrayList<Header>();
for (int i = 0; i < adapter.getCount(); i++)
mHeaders.add((Header) adapter.getItem(i));
}
super.setListAdapter(new HeaderAdapter(this, mHeaders));
}
}
SubSettings.class
public class SubSettings extends Settings {
@Override
public boolean onNavigateUp() {
finish();
return true;
}
}
Layout preference_header_item
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:background="?android:attr/activatedBackgroundIndicator"
android:gravity="center_vertical"
android:minHeight="48.0dip"
android:paddingRight="?android:scrollbarSize" >
<ImageView
android:id="@android:id/icon"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:layout_marginLeft="6.0dip"
android:layout_marginRight="6.0dip" />
<TextView
android:id="@android:id/title"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginBottom="6.0dip"
android:layout_marginLeft="2.0dip"
android:layout_marginRight="6.0dip"
android:layout_marginTop="6.0dip"
android:ellipsize="marquee"
android:fadingEdge="horizontal"
android:singleLine="true"
android:textAppearance="?android:textAppearanceMedium" />
</LinearLayout>
Android Manifest
<!-- Settings -->
<activity
android:name=".Settings"
android:hardwareAccelerated="true"
android:launchMode="singleTask"
android:taskAffinity="com.achep.example" />
<activity
android:name=".SubSettings"
android:parentActivityName="Settings" />
PS: Да, конечно краток мой рассказ получился… Учиться и еще раз учиться :) Задавайте вопросы в теме, если что-то не понятно
Автор: AChep