Wednesday, August 24, 2011

Custom ScrollBar Preference

Android's default ScrollBarPreference is just a popup window with a scrollbar - no title and no explaination. This tutorial will show how to make a custom ScrollBarPreference with a title, description, scrollbar, and other key features.

1. To begin, add some values to res/values/strings.xml.

<?xml version="1.0" encoding="utf-8"?>
<!-- Phil Brown -->
<resources>
   <string name="app_name">MyGreatApp</string>
   <string name="user_settings">user_settings</string>
   <string name="app_settings">MyGreatApp Settings</string>
   <string name="prefs_account">Account Settings</string>
   <string name="prefs_app">Application Settings</string>
   <string name="prefs_other">Other</string>
   
   <string name="option_1_key">option_1_key</string>
   <string name="option_1_title">Option 1</string>
   <string name="option_1_summary">This summary describes what this bar is for.</string>
   
   <string name="option_2_key">option_2_key</string>
   <string name="option_2_title">Option 2</string>
   <string name="option_2_summary">This summary describes what option 2 does.</string>
   <string name="app_info_key">app_info</string>
   <string name="app_info_title">Application Info</string>
   <string name="app_info_summary">copyright, version, etc</string>
   
   <string name="app_copyright_key">app_copyright</string>
   <string name="app_copyright_title">© 2011</string>
   <string name="app_copyright_summary">My Company</string>
   
   <string name="app_version_key">app_version</string>
   <string name="app_version_title">version</string>

</resources>

Modify the Strings "app_name""app_settings""option_1_key""option_1_title""option_1_summary""option_2_key""option_2_title""option_2_summary""app_copyright_summary", and "app_copyright_summary" to better reflect your company and your preferences.


2. Next, create your main preferences XML (res/layout/preferences.xml):

<?xml version="1.0" encoding="utf-8"?>
<!-- Phil Brown -->
<PreferenceScreen xmlns:android="http://schemas.android.com/apk/res/android">

    <PreferenceCategory
            android:title="@string/prefs_app" >
            
       <PreferenceScreen
            android:key="@string/option_1_key"
            android:title="@string/option_1_title"
            android:summary="@string/option_1_summary" />
            
        <PreferenceScreen
            android:key="@string/option_2_key"
            android:title="@string/option_2_title"
            android:summary="@string/option_2_summary" />
            
    </PreferenceCategory>

    <PreferenceCategory
            android:title="@string/prefs_other">
            
        <PreferenceScreen
                android:key="@string/app_info_key"
                android:title="@string/app_info_title"
                android:summary="@string/app_info_summary">

            <Preference
                android:key="@string/app_copyright_key"
                android:title="@string/app_copyright_title"
                android:summary="@string/app_copyright_summary" />

            <Preference
                android:key="@string/app_version_key"
                android:title="@string/app_version_title" />

        </PreferenceScreen>
        
        <PreferenceScreen android:title="Contact us" >

            <intent android:action="android.intent.action.SENDTO"
                    android:data="mailto:myemailaddress@someplace.com" />

        </PreferenceScreen>
        
        <PreferenceScreen android:title="Check for updates" >

            <intent android:action="android.intent.action.VIEW"
                    android:data="market://details?id=com.example.android" />

        </PreferenceScreen>

    </PreferenceCategory>

</PreferenceScreen>


Replacing the contact and update intents with correct mailto and market links.



3. Create the file res/layout/seekbar_preference.xml with the following contents:




<?xml version="1.0" encoding="utf-8"?>
<!-- Phil Brown -->
<LinearLayout
  xmlns:android="http://schemas.android.com/apk/res/android"

  android:layout_width="fill_parent"
  android:layout_height="wrap_content"
  android:orientation="vertical" >
  <TextView
    android:id="@+id/seekbar_title"
    android:text="test"
    android:layout_width="fill_parent"
    android:layout_height="wrap_content"
    android:paddingLeft="10dip"
    android:paddingRight="10dip"
    android:paddingTop="10dip"
    android:textSize="20dip" />
  <TextView
    android:id="@+id/seekbar_summary"
    android:text="test"
    android:layout_width="fill_parent"
    android:layout_height="wrap_content"
    android:paddingLeft="10dip"
    android:paddingRight="10dip"
    android:textColor="#0C68D9" />
  <SeekBar
    android:id="@+id/seekbar"
    android:layout_width="fill_parent"
    android:layout_height="wrap_content"
    android:padding="20dip"
    android:max="100" />
  <LinearLayout
    android:orientation="horizontal"
    android:layout_width="fill_parent"
    android:layout_height="30dip" >
    <TextView
      android:id="@+id/min_value"
      android:layout_width="50dip"
      android:layout_height="20dip"
      android:layout_gravity="left"
      android:paddingLeft="20dip" />
  </LinearLayout>
  <LinearLayout
    android:orientation="horizontal"
    android:layout_width="fill_parent"
    android:layout_height="wrap_content" >
    <Button
      android:id="@+id/save_slider"
      android:layout_width="120dip"
      android:layout_height="50dip"
      android:paddingLeft="10dip"
      android:text="Save" >
    </Button>
    <View 
      android:layout_width="10dip"
      android:layout_height="50dip" >
    </View>
    <Button
      android:id="@+id/cancel_slider"
      android:layout_width="120dip"
      android:layout_height="50dip"
      android:paddingRight="10dip"
      android:text="Cancel" >
    </Button>
  </LinearLayout>
</LinearLayout>


4. Create a new class called Constants with the following contents:

/** Contains contants.
 * @author Phil Brown
 */
public class Constants {
    /** String used for packing and unpacking Intent extras for the
     * SeekBarPreference activity */
    public static final String PREFERENCE_BAR_TITLE = "title",
                               PREFERENCE_BAR_SUMMARY = "summary",
                               PREFERENCE_BAR_MID = "default_value",
                               PREFERENCE_BAR_KEY = "key";
}//Constants




5. Create a new Activity called SeekBarPreference.java with the following contents:


import android.app.Activity;
import android.content.Intent;
import android.os.Bundle;
import android.view.View;
import android.view.View.OnClickListener;
import android.view.Window;
import android.widget.Button;
import android.widget.SeekBar;
import android.widget.SeekBar.OnSeekBarChangeListener;
import android.widget.TextView;
import android.content.SharedPreferences;

/**
 * Activity for changing settings using a SeekBar.
 * @author Phil Brown
 */
public class SeekBarPreference extends Activity {

   /** The key of the SharedPreferences value*/
   private String key;
   /** The title of the settings option */
   private String title;
   /** The summary of the settings option */
   private String summary;
   /** The seekbar */
   private SeekBar seekbar;
   /** The seekbar's average value */
   private int bar_mid;
   /** The save settings button */
   private Button save_button;
   /** The cancel changes button */
   private Button cancel_button;
   /** Contains user settings using key/value pairing. */
   private SharedPreferences user_prefs;
   /** Allows edit access to {@link #user_prefs}
   private SharedPreferences.Editor user_preferences;

   /** Initializes the layout*/
   @Override
   public void onCreate(Bundle savedInstanceState) {
      super.onCreate(savedInstanceState);
      requestWindowFeature(Window.FEATURE_NO_TITLE);
      setContentView(R.layout.seekbar_preference);
      this.seekbar = (SeekBar) findViewById(R.id.seekbar);
      final TextView progress_value = (TextView) findViewById(R.id.min_value);
      progress_value.setText(Integer.toString(this.seekbar.getProgress()));
      this.seekbar.setOnSeekBarChangeListener(new OnSeekBarChangeListener() {

         @Override
         public void onProgressChanged(SeekBar seekBar, int progress, 
                                                        boolean fromUser) {
            progress_value.setText(Integer.toString(progress));         }

         @Override
         public void onStartTrackingTouch(SeekBar seekBar) {}

         @Override
         public void onStopTrackingTouch(SeekBar seekBar) {}
   
      });
      this.handleNewIntent(this.getIntent());
      ((TextView) findViewById(R.id.seekbar_title)).setText(this.title);
      ((TextView) findViewById(R.id.seekbar_summary)).setText(this.summary);
      save_button = (Button) findViewById(R.id.save_slider);
      cancel_button = (Button) findViewById(R.id.cancel_slider);
      save_button.setOnClickListener(new OnClickListener() {

         @Override
         public void onClick(View v) {
            save();
         }
       
      });
      cancel_button.setOnClickListener(new OnClickListener() {

         @Override
         public void onClick(View v) {
            finish();
         }
       
      });
      this.user_prefs = getSharedPreferences(getString(R.string.user_settings),
                                             Activity.MODE_PRIVATE);
     
      this.user_preferences = this.user_prefs.edit();
   }//onCreate
 
   /** Unpacks the new Intent */
   public void handleNewIntent(Intent intent) {    
      this.key = intent.getStringExtra(Constants.PREFERENCE_BAR_KEY);
      this.title = intent.getStringExtra(Constants.PREFERENCE_BAR_TITLE);
      this.summary = intent.getStringExtra(Constants.PREFERENCE_BAR_SUMMARY);
  
      this.bar_mid = intent.getIntArrayExtra(Constants.PREFERENCE_BAR_MID);     
      this.seekbar.setMax(this.bar_mid * 2);
      this.seekbar.setProgress(G.user_prefs.getInt(this.key, this.bar_mid));
   }//handleNewIntent

   /** Saves the user settings to the SharedPreferences. If the user sets
    * the progress to zero, use 1 instead. */
   public void save(){
      this.user_preferences.putInt(this.key, this.seekbar.getProgress());
      this.user_preferences.commit();
      finish();
   }//save
   
}//SeekBarPreference
6. Now add the following lines to the Android Manifest inside the application attribute:

<activity android:name=".SeekBarPreference"
          android:label="@string/app_settings"
          android:launchMode="singleTop"
          android:theme="@android:style/Theme.Dialog" >
</activity>


7. Lastly, create the class Preferences.java. This is your main Preference class:


package org.cyclopath.android;

import org.cyclopath.android.conf.Constants;

import android.content.Intent;
import android.content.pm.PackageManager.NameNotFoundException;
import android.os.Bundle;
import android.preference.Preference;
import android.preference.PreferenceActivity;

/**
 * User Preferences Activity
 * @author Phil Brown
 */
public class Preferences extends PreferenceActivity {

   /** Intent for modifying option 1 */
   private Intent option_1_settings;
   /** Intent for modifying option2 */
   private Intent option_2_settings;
 
   @Override
   /** Initialize the layout */
   protected void onCreate(Bundle savedInstanceState) {
      super.onCreate(savedInstanceState);
      addPreferencesFromResource(R.layout.preferences);
      Preference version = (Preference)
                           findPreference(getString(R.string.app_version_key));
      String version_name;
      try {
         version_name = getPackageManager()
                        .getPackageInfo(getPackageName(), 0)
                        .versionName;
      } catch (NameNotFoundException e) {
         version_name = "unknown";
      }
      version.setSummary(version_name);
   }//onCreate

   /** Initialize the Intents and set the preference Intents to open a new */
   @Override
   public void onStart() {
      super.onStart();
      this.initializeIntents();
      ((Preference) findPreference(getString(R.string.option_1_key)))
                    .setIntent(option_1_settings);
      ((Preference) findPreference(getString(R.string.option_2_key)))
                    .setIntent(option_2_settings);
   }//onStart
 
   /** Initialize the Intents in order to correctly launch a new SeekBarPreference */
   public void initializeIntents() {
      gps_main_settings = new Intent(this, SeekBarPreference.class);
      gps_main_settings.putExtra(Constants.PREFERENCE_BAR_TITLE,
                                 getString(R.string.option_1_title));
      gps_main_settings.putExtra(Constants.PREFERENCE_BAR_SUMMARY,
                                 getString(R.string.option_1_summary));
      gps_main_settings.putExtra(Constants.PREFERENCE_BAR_KEY,
                                 getString(R.string.option_1_key));
      gps_main_settings.putExtra(Constants.PREFERENCE_BAR_MID, 50);
       
      gps_service_settings = new Intent(this, SeekBarPreference.class);
      gps_service_settings.putExtra(Constants.PREFERENCE_BAR_TITLE,
                                    getString(R.string.option_2_title));
      gps_service_settings.putExtra(Constants.PREFERENCE_BAR_SUMMARY,
                                    getString(R.string.option_2_summary));
      gps_service_settings.putExtra(Constants.PREFERENCE_BAR_KEY,
                                    getString(R.string.option_2_key));
      gps_service_settings.putExtra(Constants.PREFERENCE_BAR_MID, 50);
   }//initializeIntents
 
}//Preferences