Настройки в стиле Holo Android

в 16:14, , рубрики: achep, fragments, header, holo, settings, настройки, Разработка под android, метки: , , , , , ,

Итак, решил написать небольшой пост(я не умелец в этом, поэтому — много кода, мало слов) о том, как сделать настройки как в официальном приложении Настройки в Android 4 (может и в 3.0 тоже). Наша цель:
0. Умение читать и понимать код без объяснений
1. Использование фрагментов
2. Использование header'ов
3. Разделение пунктов на категории
4. Поддержка всех разрешений экрана
5. Использовать SDK14

image

Фрагменты

Я предполагаю, что вы уже ознакомлены с таким понятием, как фрагменты в 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

Просмотреть полный исходный код 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

Просмотреть полный исходный код SubSettings.class

public class SubSettings extends Settings {

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

Layout preference_header_item

Просмотреть полный исходный код preference_header_item.xml

<?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

Поделиться

* - обязательные к заполнению поля