9

How do I make the settings navigation for SwitchReference like Android native settings app?

Where when u click on WiFi region(not on the switch) it will navigate to a new screen:

Click on WiFi

My app only change the switch from ON to OFF and vice versa even when I'm not clicking on the switch.

I'm using PreferenceFragment and xml for the screen. And my preference is following the example from Android PreferenceFragment documentation. I develop my app on ICS 4.0 API 14.

Anyone know how to do this?

Edited:

My XML look like this:

<PreferenceScreen xmlns:android="http://schemas.android.com/apk/res/android" >

    <PreferenceCategory
        android:layout="@layout/preference_category"
        android:title="User Settings" >
        <SwitchPreference
            android:key="pref_autorun"
            android:layout="@layout/preference"
            android:summary="Autorun SMODE on boot"
            android:title="Autorun SMODE" />
        <SwitchPreference
            android:key="pref_wifi_control"
            android:layout="@layout/preference"
            android:selectable="false"
            android:summary="Controls your Wi-Fi radio automatically based on hotspot availability"
            android:title="Wi-Fi Radio Control" />
    </PreferenceCategory>
</PreferenceScreen> 
Zul
  • 1,344
  • 2
  • 12
  • 21

3 Answers3

5

By taking a look at the source of the stock Settings app, you can find how they did it.

Basically, they use a custom ArrayAdapter (much like you would do with a ListView) to display rows with Switch buttons. And for the second screen, they simply use the CustomView available in the ActionBar.

I wrote an article with a sample code to show how you can do it in your project. Be careful though, this can only wirk in API Level 14 or higher, so if you target older devices, keep an old style preference screen.

XGouchet
  • 10,002
  • 10
  • 48
  • 83
  • Nice. Having said that it's ridiculous have much you have to code for a simple Preference screen AND to get it to work for old, new and tablets. One should just have to declare it in one xml file and it SHOULD work everywhere. Period! – powder366 Feb 23 '13 at 15:06
  • This solutions looks way too big for such a small problem. Is there any alternative to determine when the user clicked on the checkbox or in the text area? – MatheusJardimB Feb 03 '14 at 16:50
1

I see 2 questions here: 1. How to listen to preference click outside the switch area? 2. How to put a switch in the actionbar?

I'll answer question 1:

I created a new SwitchPreference class and overrode the onClick method to do nothing. This prevents the setting change when clicking outside the switch.

public class SwitchPreference extends android.preference.SwitchPreference {
    @Override
    protected void onClick() {
    }
}

Usage (xml):

<android.util.SwitchPreference
    android:key="whatever"
    android:title="Whatever" />

Usage (java):

SwitchPreference switchPreference = (SwitchPreference) findPreference("whatever");
switchPreference.setOnPreferenceClickListener(new OnPreferenceClickListener() {
    @Override
    public boolean onPreferenceClick(Preference preference) {
        DialogUtils.showToast(Preferences.this, "Clicked outside switch");
        return true;
    }
});
switchPreference.setOnPreferenceChangeListener(new OnPreferenceChangeListener() {
    @Override
    public boolean onPreferenceChange(Preference preference, Object newValue) {
        DialogUtils.showToast(Preferences.this, "Clicked inside switch and setting changed");
        return true;
    }
});
AlikElzin-kilaka
  • 34,335
  • 35
  • 194
  • 277
  • I like how its a nice simple answer, but when I tried this code the "Clicked inside switch and setting changed" branch was not taken even if I click on the switch part. – Marc Jan 11 '15 at 16:40
1

I gave this one a try and struggled a bit. I thought I would share my experience.

At first I tried the answer by XGouchet since it had the most up votes. The solution was fairly complicated and required using preference headers which are very cool but did not fit what I was doing. I decided the easiest thing for me to do is to wrap up my preferenceFragment in a regular fragment and make fake the switch preference with a regular view. I dug up the android source for a preference and came up with this

 <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
tools:context="com.lezyne.link.ui.homeScreen.settingsTab.SettingsFragment">

<FrameLayout
    android:id="@+id/fragment_container"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:layout_gravity="center_horizontal">


</FrameLayout>

<View
    android:layout_width="fill_parent"
    android:layout_height="1dp"
    android:layout_marginLeft="15dp"
    android:layout_marginRight="15dp"
    android:background="#e1e1e1" />

<LinearLayout
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:background="?android:attr/selectableItemBackground"
    android:gravity="center_vertical"
    android:minHeight="?android:attr/listPreferredItemHeight"
    android:orientation="horizontal"
    android:paddingRight="?android:attr/scrollbarSize">

    <ImageView
        android:id="@+id/icon"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_gravity="center" />

    <RelativeLayout
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_marginBottom="6dip"
        android:layout_marginLeft="15dip"
        android:layout_marginRight="6dip"
        android:layout_marginTop="6dip"
        android:layout_weight="1">

        <TextView
            android:id="@+id/title"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:ellipsize="marquee"
            android:fadingEdge="horizontal"
            android:singleLine="true"
            android:textAppearance="?android:attr/textAppearanceLarge" />

        <TextView
            android:id="@+id/summary"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_alignLeft="@android:id/title"
            android:layout_below="@android:id/title"
            android:maxLines="4"
            android:textAppearance="?android:attr/textAppearanceSmall"
            android:textColor="?android:attr/textColorSecondary" />

    </RelativeLayout>

    <!-- Preference should place its actual preference widget here. -->
    <RelativeLayout
        android:id="@+id/widget_frame"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:gravity="center_vertical"
        android:orientation="vertical"
        android:paddingEnd="36dp">


        <Switch
            android:thumb="@drawable/switch_inner"
            android:id="@+id/switch1"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_alignParentEnd="true" />

        <TextView
            android:id="@+id/textView6"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_marginLeft="2dp"
            android:text="@string/Notifications"
            android:textAppearance="?android:attr/textAppearanceMedium" />

    </RelativeLayout>

</LinearLayout>


<View
    android:layout_width="fill_parent"
    android:layout_height="1dp"
    android:layout_marginLeft="15dp"
    android:layout_marginRight="15dp"
    android:background="#e1e1e1" />


 </LinearLayout>

The FrameLayout at the top gets a preference fragment like so

    getChildFragmentManager()
            .beginTransaction()
            .replace(R.id.fragment_container, new SettingsPreferencesFragment(), "SettingsPreferencesFragment")
            .commit();

Then I can get assign my click listeners as I would in any regular view. SettingsPreferencesFragment is just totally standard preference fragment.

This solution seemed pretty good, but then I noticed weird layout issues on tablets. I realized that I would have trouble getting this solution to look right on all devices and I needed to use a real switchPreference not a fake one.

============== Solution 2 ===============

AlikElzin-kilaka's solution was nice and simple but did not work when I tried. I tried a 2nd time to make sure that I had not just done it wrong. I kept playing around and came up with something that seems to work. He has a good point about

2 questions here: 1. How to listen to preference click outside the switch area? 2. How to put a switch in the actionbar?

Really only question (1) is worth answering because question 2 has been answered here and other places

I realized the only way to get access to the views in a preference was to create a child class and override onBind. So I came up with this child class of SwitchPreference that creates separate click handlers for the switch as the entire view. Its still a hack though.

public class MySwitchPreference extends SwitchPreference {
public MySwitchPreference(Context context, AttributeSet attrs, int defStyleAttr) {
    super(context, attrs, defStyleAttr);
}


public MySwitchPreference(Context context, AttributeSet attrs) {
    super(context, attrs);
    getView(null,null);

}

public MySwitchPreference(Context context) {
    super(context);
}

public interface SwitchClickListener{
    public void onSwitchClicked(boolean checked);
    public void onPreferenceClicked();
}
private SwitchClickListener listener = null;
public void setSwitchClickListener(SwitchClickListener listener){
    this.listener = listener;
}

public Switch findSwitchWidget(View view){
    if (view instanceof  Switch){
        return (Switch)view;
    }
    if (view instanceof ViewGroup){
        ViewGroup viewGroup = (ViewGroup)view;
        for (int i = 0; i < viewGroup.getChildCount();i++){
            View child = viewGroup.getChildAt(i);
            if (child instanceof ViewGroup){
                Switch result = findSwitchWidget(child);
                if (result!=null) return result;
            }
            if (child instanceof Switch){
                return (Switch)child;
            }
        }
    }
    return null;
}

protected void onBindView (View view){
    super.onBindView(view);

    final Switch switchView = findSwitchWidget(view);
    if (switchView!=null){
        switchView.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                if (listener!=null) listener.onSwitchClicked(switchView.isChecked());
            }
        });
        switchView.setFocusable(true);
        switchView.setEnabled(true);
    }

    view.setOnClickListener(new View.OnClickListener() {
        @Override
        public void onClick(View v) {
            if (listener!=null) listener.onPreferenceClicked();
        }
    });

}
} 

It uses the recursive function findSwitchWidget to walk the tree until it finds a Switch. I would have rather write code like this:

Switch switchView = view.findViewById(android.R.id.switchView);

but there does not seem to be a way to get to that internal id value that I know of. Anyhow once we have the actual switch we can assign listeners to it and the container view. The switch preference won't update automatically so its necessary to save the preference yourself.

MySwitchPreference switchPreference = (MySwitchPreference) findPreference("whatever");
    switchPreference.setSwitchClickListener(new MySwitchPreference.SwitchClickListener() {
        @Override
        public void onSwitchClicked(boolean checked) {
            //Save the preference value here 
        }

        @Override
        public void onPreferenceClicked() {
            //Launch the new preference screen or activity here
        }
    });

Hopefully this 2nd hack wont come back to bite me.

Any one see any potential pitfalls of this method?

I also put a slightly improved version of the code on github https://gist.github.com/marchold/45e22839eb94aa14dfb5

Community
  • 1
  • 1
Marc
  • 1,159
  • 17
  • 31