📜 ⬆️ ⬇️

Holo Android Settings

So, I decided to write a small post (I’m not skilled at this, therefore - a lot of code, few words) on how to make the settings as in the official Settings application in Android 4 (maybe in 3.0 too). Our goal:
0. The ability to read and understand the code without explanation
1. Using Fragments
2. Using headers
3. The division of items into categories
4. Support all screen resolutions
5. Use SDK14

image

Fragments (menu items on the main screen)



What are fragments? Why use them? The fragments are part of “one Android for tablets and phones”, they allow you to create an application that looks good on the phone and on the tablet faster and more elegantly.
I assume that you are already familiar with them, otherwise - an example of the simplest fragment with the settings from the xml-file, which will open the habr by clicking on the desired menu item
')
src / com / achep / example / TestFragment.class
public class TestFragment extends PreferenceFragment implements onPreferenceClickListener { /** *   "  " */ private static final String KEY_HABRAHABR_LAUNCHER = "habrahabrLauncher"; /** *  "  " */ private Preference mHabrahabrLauncher; @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); //     PreferenceActivity addPreferencesFromResource(R.xml.test_settings); mHabrahabrLauncher = (Preference) findPreference(KEY_HABRAHABR_LAUNCHER); mHabrahabrLauncher.setOnPreferenceClickListener(this); } @Override public boolean onPreferenceClick(Preference preference) { if (preference == mHabrahabrLauncher) { //     startActivity(new Intent(Intent.ACTION_VIEW, Uri.parse("http://habrahabr.ru"))); } else { //    } return false; } } 


Main Settings Screen: Resources



An example of constructing an xml-file (resources) of our main settings menu, which contains our fragment (in triplicate) and two categories

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:id="@+id/header_test" //     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> 


As you can see, the Items do not differ in type from Categories, which, of course, is a little upsetting, but we will determine what is what, by attributes (you can use all sorts of things: id, fragment, intent, and many others), and not types in already familiar resources for PreferenceActivity)

Main settings screen: Java code



This code is based on the official application code , which I took on GitHub and modified to fit my needs. The most important part that I advise you to pay attention to is the HeaderAdapter, to implement the items in it, you have to be a little tricky to act just like Google. We will not understand how the Switch-points are arranged in the official application, since their use is very specific.

src / com / achep / example / 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 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; //    } @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 is based on the Settings class (differs only by the “Back” button) and is used to navigate between menu items in a non-tablet device.

src / com / achep / example / SubSettings.class
 public class SubSettings extends Settings { //    "" @Override public boolean onNavigateUp() { finish(); return true; } } 


Main settings screen: Layout simple item



To show just the item, I use my Layout, in which I deleted everything that I didn’t use and left only the title and icon. Actually with this Layout you will not be able to write a subtitle, but with this (for example) you can

layout / 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



And the final touch - add the following data in the manifest

AndroidManifest.xml
  <!-- Settings --> <activity android:name=".Settings" android:hardwareAccelerated="true" android:launchMode="singleTask" android:taskAffinity="com.achep.example" /> <activity android:name=".SubSettings" android:parentActivityName="Settings" /> 


Bonus: Run the specified item in the settings



In this case, we will run our TestFragment.

Add to
src / com / achep / example / Settings.class the following lines:
  public static class TestFragmentActivity extends Settings { /* empty */ } 

and in
AndroidManifest.xml
  <activity android:name=".Settings$TestFragmentActivity" android:clearTaskOnLaunch="true" android:parentActivityName="Settings" > <meta-data android:name="com.achep.example.settings.FRAGMENT_CLASS" android:value="com.achep.example.TestFragment" /> <meta-data android:name="com.achep.stopwatch.TOP_LEVEL_HEADER_ID" android:resource="@id/header_test" /> 

and that's it :) Naturally, instead of running Settings.class, you will need to run Settings.TestFragmentActivity.class

PS: If I did not tell something, something is not clear - ask questions in the subject.

Source: https://habr.com/ru/post/151003/


All Articles