I come to you on bended knee, question in hand. I am relatively new to Android, so pardon any sacrilegious things I might say.
Intro: I have several layouts in the app, that all have to include a common footer. This footer has some essential buttons for returning to the home page, logging out, etc.
I managed to get this footer to appear in all the requisite pages with the help of the Include and Merge tags. The issue lies in defining on click listeners for all the buttons. Although I can define the listeners in every activity associated with screens that include the footer layout, I find that this becomes terribly tedious when the number of screens increases.
My question is this: Can I define a button click listener that will work across the application, which can be accessed from any screen with the use of the android:onClick attribute of the Button?
That is to say, I would like to define the button click listener once, in a separate class, say FooterClickListeners, and simply name that class as the listener class for any button clicks on the footer. The idea is to make a single point of access for the listener code, so that any and all changes to said listeners will reflect throughout the application.
I had the same problem with a menu which I used in several layouts. I solved the problem by inflating the layout xml file in a class extending RelativeLayout where I then defined the onClickListener. Afterwards I included the class in each layout requiring the menu. The code looked like this:
menu.xml
<?xml version="1.0" encoding="utf-8"?>
<merge xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="fill_parent"
android:layout_height="fill_parent">
<ImageButton android:id="#+id/map_view"
android:layout_alignParentTop="true"
android:layout_alignParentLeft="true"
android:src="#drawable/button_menu_map_view"
android:background="#null"
android:scaleType="fitCenter"
android:layout_height="#dimen/icon_size"
android:layout_width="#dimen/icon_size">
</ImageButton>
<ImageButton android:id="#+id/live_view"
android:layout_alignParentTop="true"
android:layout_alignParentLeft="true"
android:src="#drawable/button_menu_live_view"
android:background="#null"
android:scaleType="fitCenter"
android:layout_height="#dimen/icon_size"
android:layout_width="#dimen/icon_size">
</ImageButton>
<ImageButton android:id="#+id/screenshot"
android:layout_alignParentTop="true"
android:layout_alignParentRight="true"
android:src="#drawable/button_menu_screenshot"
android:background="#null"
android:scaleType="fitCenter"
android:layout_height="#dimen/icon_size"
android:layout_width="#dimen/icon_size">
</ImageButton>
</merge>
MenuView.java
public class MenuView extends RelativeLayout {
private LayoutInflater inflater;
public MenuView(Context context, AttributeSet attrs) {
super(context, attrs);
inflater = (LayoutInflater)context.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
inflater.inflate(R.layout.menu, this, true);
((ImageButton)this.findViewById(R.id.screenshot)).setOnClickListener(screenshotOnClickListener);
((ImageButton)this.findViewById(R.id.live_view)).setOnClickListener(liveViewOnClickListener);
((ImageButton)this.findViewById(R.id.map_view)).setOnClickListener(mapViewOnClickListener);
}
private OnClickListener screenshotOnClickListener = new OnClickListener() {
public void onClick(View v) {
getContext().startActivity(new Intent(getContext(), ScreenshotActivity.class));
}
};
private OnClickListener liveViewOnClickListener = new OnClickListener() {
public void onClick(View v) {
getContext().startActivity(new Intent(getContext(), LiveViewActivity.class));
}
};
private OnClickListener mapViewOnClickListener = new OnClickListener() {
public void onClick(View v) {
getContext().startActivity(new Intent(getContext(), MapViewActivity.class));
}
};
}
layout.xml
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:id="#+id/main"
android:orientation="vertical"
android:layout_width="fill_parent"
android:layout_height="fill_parent">
<SurfaceView android:id="#+id/surface"
android:layout_width="fill_parent"
android:layout_weight="1"
android:layout_height="fill_parent">
</SurfaceView>
<!-- some more tags... -->
<com.example.inflating.MenuView
android:layout_width="fill_parent"
android:layout_height="fill_parent" />
</RelativeLayout>
with the <com.example.inflating.MenuView /> tag, you are now able to reuse your selfwritten Layout (incl onClickListener) in other layouts.
This is something that is getting added to roboguice in the near the future. It will allow you to build controller classes for things like titlebar's and footers and have the events autowired for you.
Checkout http://code.google.com/r/adamtybor-roboguice/ for the initial spike.
Basically if you are using roboguice you can define a component for footer and just inject that footer component into each activity.
Unfortunately you still have to add the controller to every activity, just like you did with the include layout, but the good news is everything gets wired up for you and all your logic stays in a single class.
Below is some pseudo code of some example usage.
public class FooterController {
#InjectView(R.id.footer_button) Button button;
#Inject Activity context;
#ContextObserver
public void onViewsInjected() {
button.setOnClickListener(new OnClickListener() {
void onClick() {
Toast.makeToast(context, "My button was clicked", Toast.DURATION_SHORT).show();
}
});
}
}
public class MyActivity1 extends RoboActivity {
#Inject FooterController footer;
}
public class MyActivity2 extends RoboActivity {
#Inject FooterController footer;
}
The solution as you describe is impossible, sorry. But you can have common parent activity for all your activities that use the footer. In the activity just provide handler methods for your footer buttons, then just inherit from it every time you need to handle the footer actions.
Related
I want to create a custom Compound Control in Android that holds some logic. For the purpose of this example, let's say I want it to switch between two views when clicked.
According to the API guide, it looks like the way to do that is to create a new class that extends Layout, and do everything in there.
So I did just that:
I created a XML layout to inflate for my custom component:
.
<RelativeLayout
android:layout_width="match_parent"
android:layout_height="match_parent">
<TextView
android:id="#+id/view1"
android:layout_height="wrap_content"
android:layout_width="wrap_content"
android:text="Hello"/>
<TextView
android:id="#+id/view2"
android:layout_height="wrap_content"
android:layout_width="wrap_content"
android:text="World"
android:visibility="gone"/>
</RelativeLayout>
Then I created my custom Layout class, and added the logic in there:
public class MyWidget extends RelativeLayout {
public final View mView1;
public final View mView2;
public MyWidget(Context context, AttributeSet attrs) {
super(context, attrs);
LayoutInflater inflater = (LayoutInflater)context.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
RelativeLayout view = (RelativeLayout) inflater.inflate(R.layout.my_widget, this, true);
mView1 = view.findViewById(R.id.view1);
mView2 = view.findViewById(R.id.view2);
view.setOnClickListener(new OnClickListener() {
#Override
public void onClick(View v) {
switchViews();
}
});
}
public void switchViews() {
if (mView1.getVisibility() == View.VISIBLE) {
mView1.setVisibility(View.GONE);
} else {
mView1.setVisibility(View.VISIBLE);
}
if (mView2.getVisibility() == View.VISIBLE) {
mView2.setVisibility(View.GONE);
} else {
mView2.setVisibility(View.VISIBLE);
}
}
}
And finally, I included my custom view in some layout:
.
<RelativeLayout
android:layout_width="match_parent"
android:layout_height="wrap_content">
<com.example.MyWidget
android:layout_height="match_parent"
android:layout_width="match_parent"/>
</RelativeLayout
And that works.
I am not completely happy with that solution though, for 2 reasons:
In the constructor of MyWidget, I instantiate 2 nested RelativeLayout by calling the super() constructor, and the one that is at the root of my XML layout. For that, I know I can instead use <merge> as my XML root and that gets me rid of the extra RelativeLayout. Except that defining XML attributes, such as android:background on my <merge> tag doesn't have any effect, so I have to define it programmatically, which is not as nice.
The custom View is a subclass of RelativeLayout, and therefore expose methods it inherits from it, such as addView(), even if it doesn't make sense to add child views to it. I know I can override those methods to prevent users from doing that, but I would still find it cleaner to inherit from View.
I have an template view it looks like ;
template.xml :
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:id="#+id/contentLayout"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="#color/view_background"
android:gravity="center_vertical"
tools:context=".MainActivity" >
<Button
android:id="#+id/btnMultiple"
android:layout_width="0dp"
android:layout_height="150dp"
android:layout_marginLeft="5dp"
android:layout_marginRight="5dp"
android:layout_weight="1"
android:background="#drawable/button_backgroundblue"
android:onClick="btnMultiple_clicked"
android:tag="4"
android:text="#string/mc_abcd"
android:textColor="#drawable/button_textcolor"
android:textSize="#dimen/h2" />
I'm creating this view programmatically and then i'm adding this View into ViewFlipper as like ;
activity_main.java
public void btnCreateView_clicked(View view) {
ViewFlipper viewFlipper = (ViewFlipper)findViewById(R.id.flipper);
View myView = LayoutInflater.from(getApplicationContext()).inflate(R.layout.template, viewFlipper, false);
viewFlipper.addView(myView);
viewFlipper.showNext(); // Showing lastly created myView successfully. }
Also my activity has public function as like ;
activity_main.java
public void btnMultiple_clicked(View view) {
//Save the world !
}
When i press btnCreateView button from first view it's successfully creating template (myView) and then adding it into ViewFlipper. When i click btnMultiple i'm getting error and my application crashes ; "Could not find a method btnMultiple_clicked(View) in the activity class android.app.Application for onClick handler on view class android.widget.Button with id 'btnMultiple'" i'm sure there is existing method btnMultiple_clicked(View).
PS! If i add listener to btnMultiple programmatically it's gonna work but i wonder why "android:onClick="btnMultiple_clicked"" doesn't work ?
Android looks for the method defined in the onClick attribute in the Activity. The problem in your code is that you used for the LayoutInflater initialization the Application's Context and not the Context of the Activity so the method will not be found there(the Context is passed to the View). Use:
View myView = LayoutInflater.from(this).inflate(R.layout.template, viewFlipper, false);
or any other reference that points to the Activity where those buttons will be used.
This is a good example why you should use in most cases the Context of the Activity.
I've tried to approach this from different angles without luck. Maybe asking a general question can help.
Technically I'm using osmdroid's MapView implementation, not Google's Maps API, but I think this question is a more general programmatic Views vs main_activity.xml defined views in onCreate.
Basically in my MainActivity if I onCreate a View, like MapView, then set it as the ContentView programmatically, I have to also programmatically add in any other Views I want to display in my app:
public void onCreate(final Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
this.mapView = new MapView(this, 256);
...
this.setContentView(this.mapView);
}
If I attempt to set the ContentView as activity_main, the MapView can't be adjusted onCreate. Maybe I'm missing something: (note that I have methods that handle loading a custom offline tile set, and place markers on the map, etc...)
public void onCreate(final Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
this.mapView = (MapView)findViewById(R.id.mapview);
...
this.intializeMapTiles();
this.mapView.setBuiltInZoomControls(true);
this.mapView.setMultiTouchControls(true);
this.mapController.setCenter(new GeoPoint((int)(50.349622 * 1E6), (int)(-71.823700 *1E6)));
...
this.mapView.invalidate();
}
Here's my activity_main.xml in this case:
<RelativeLayout 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"
tools:context=".MainActivity" >
<org.osmdroid.views.MapView
xmlns:android="http://schemas.android.com/apk/res/android"
android:id="#+id/mapview"
android:layout_width="fill_parent"
android:layout_height="fill_parent"
android:clickable="false"/>
</RelativeLayout>
When trying to get the MapView from the ContentView (acitivty_main), none of my method changes to it make any affect. It's as if I don't have access the exact MapView that's being rendered. I've tried invalidating my MapView but it doesn't matter. I get a default looking and behaving MapView.
The reason I'm trying to solve this is naturally I'm looking for my app to include more than a single MapView. I would like to include a SlidingDrawer, or some method of displaying a View with buttons that is only displayed when you long press on a map marker. (note that I have Toast pop ups being displayed on map marker long presses, so I'm good in this regard)
I'd have to add these other Views (SlideingDrawer, etc...) programmatically and not from the main_activity.xml. Even that has a catch-22, where the SlidingDrawer constructor needs an AttributeSet from xml that's painful to build yourself. (I tried) Then you also have to worry about the layout as well.
Anyone have any suggestions? General or otherwise? Thanks!
This might actually be useful to others for a number of reasons.
If you're stuck having to use a View that can only be configured the way to need via its programmatic constructor (e.g. you could just include the View from your activity_main.xml, but the View isn't what you need it to be unless you construct it yourself, like with offline tile maps using OSMDroid's MapView) then you're stuck extending that View and implementing that View's constructor that includes the AttributeSet. The AttributeSet is basically a structure parsed from the activity_main.xml xml for that view. That constructor will be called automatically in Activity when you this.setContentView(R.layout.activity_main) from onCreate(). So any custom constructor stuff needs to go in that constructor for your extended View.
For example, I had to extend the OSMDroid MapView, then implement my offline map tile source from the super entirely. NOTE you have to super() the 1st line in an extended constructor because object methods aren't available until after the inherited constructor is complete, so any super() method calls have to be to static methods.
public class FieldMapView extends MapView {
public FieldMapView(Context context, AttributeSet attrs) throws Exception {
super(
context,
256,
new DefaultResourceProxyImpl(context),
FieldMapView.getOfflineMapProvider(context, MainActivity.mapTileArchiveFilename),
null,
attrs);
this.setUseDataConnection(false);
this.setBuiltInZoomControls(false);
this.setMultiTouchControls(true);
}
Then in my activity_main.xml I point to the extended version of the View: (e.g. FieldMapView)
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:id="#+id/rootview"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".MainActivity" >
<com.test.FieldMapView
xmlns:android="http://schemas.android.com/apk/res/android"
android:id="#+id/mapview"
android:layout_width="fill_parent"
android:layout_height="fill_parent"
android:clickable="false"/>
So now I have my extended View taking care of any programmatic style requirements, how did I get a SlidingDrawer working with it? I created a SlidingDrawer with a 0dip height View as the handle. I then included a LinearLayout that contains buttons, whatever you want, etc...
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:id="#+id/rootview"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".MainActivity" >
<com.test.FieldMapView
xmlns:android="http://schemas.android.com/apk/res/android"
android:id="#+id/mapview"
android:layout_width="fill_parent"
android:layout_height="fill_parent"
android:clickable="false"/>
<SlidingDrawer
android:layout_width="wrap_content"
android:id="#+id/slidingDrawerActions"
android:content="#+id/action_content"
android:padding="10dip"
android:layout_height="75dip"
android:handle="#+id/handle2"
android:layout_alignBottom="#id/mapview"
android:orientation="vertical"
android:clickable="false">
<LinearLayout
android:layout_width="wrap_content"
android:id="#+id/action_content"
android:orientation="horizontal"
android:gravity="center"
android:padding="10dip"
android:background="#FF999999"
android:layout_height="wrap_content"
android:clickable="false">
<View
android:id="#id/handle2"
android:layout_width="0dip"
android:layout_height="0dip" />
<ImageButton
android:id="#+id/chatActionButton"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_margin="2dip"
android:src="#drawable/ic_action_edit"
android:text="Chat">
</ImageButton>
</LinearLayout>
</SlidingDrawer>
</RelativeLayout>
In my MainActivity onCreate() I just find the SlidingDrawer View and assign any listeners accordingly. For opening the drawer when long pressing a Marker in the MapView (this is getting more OSMDroid specific now) I naturally have an OnItemGestureListener to open the drawer:
class NodeGestureListener implements OnItemGestureListener<NodeOverlayItem> {
#Override
public boolean onItemLongPress(int index, NodeOverlayItem node) {
if(slidingDrawerActions.isOpened() || slidingDrawerActions.isMoving()) {
return false;
}
slidingDrawerActions.animateOpen();
return false;
}
The tricky part is I wanted to close it via a click on the MapView (not by touching a close button that takes up space) so I had to assign SlidingDrawer.OnDrawerOpenListener and OnDrawerCloseListener classes. They simply flipped a boolean indicating if the drawer was open or closed. I then set a simple onClickListener for the MapView that closed the drawer if it was open based on the isActionDrawerOpen set by the SlidingDrawer listeners.
public void onCreate(final Bundle savedInstanceState) {
...
this.mapView.setOnClickListener(new MapViewClickListener());
...
this.slidingDrawerActions = (SlidingDrawer)findViewById(R.id.slidingDrawerActions);
this.slidingDrawerActions.setOnDrawerOpenListener(new SlidingDrawerOpenListener());
this.slidingDrawerActions.setOnDrawerCloseListener(new SlidingDrawerCloseListener());
...
}
...
private boolean isActionDrawerOpen = false;
class SlidingDrawerOpenListener implements SlidingDrawer.OnDrawerOpenListener {
#Override
public void onDrawerOpened() {
isActionDrawerOpen = true;
}
}
class SlidingDrawerCloseListener implements SlidingDrawer.OnDrawerCloseListener {
#Override
public void onDrawerClosed() {
isActionDrawerOpen = false;
}
}
private boolean skippedMapViewClickListener = false;
class MapViewClickListener implements OnClickListener {
public void onClick(View view) {
if(isActionDrawerOpen) {
if(skippedMapViewClickListener) {
slidingDrawerActions.animateClose();
skippedMapViewClickListener = false;
} else {
skippedMapViewClickListener = true;
}
}
}
}
Note the skippedMapViewClickListener boolean. The problem I had was that the MapView OnClickListener would be called immediately after the SlidingDrawer listener when long pressing the Marker. Meaning the long press would be considered a MapView click, plus the long press itself would open the drawer before OnClickListener was called, so OnClickListener would always see the drawer as open, and would close it. What I did was effectively skip the 1st onClick this way, so the drawer would stay open until you clicked on the MapView. Seems to work great.
I hope this helps someone. There are like 4 problems I solved with this approach.
I have a preference screen that is populated with items from a database. I have this working by creating my own PreferenceActivity. In the activity I create DialogPreference items and add them to my PreferenceCategory To style to preference item on the screen I use a custom layout and apply it using setLayoutResource(R.layout.custom_pref_row)
This basically adds an ImageButton to the view aligned to the right of the layout. This all works fine and my preference screen shows the custom view with the button. My question is how do I attach a click listener to the button in the custom view? I was not able to find a way to get at View for the row from the PreferenceActivity. If my items were not created dynamically I might be able to do this all from XML and then reference the id or the button, but I can do that because I am creating the list dynamically.
Any suggestions on how to get a handle on the ImageButton for each item? In the end I want to configure the button to launch a delete confirmation dialog.
R.layout.custom_pref_row:
<?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:minHeight="?android:attr/listPreferredItemHeight"
android:gravity="center_vertical"
android:paddingRight="?android:attr/scrollbarSize">
<RelativeLayout
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginLeft="15dip"
android:layout_marginRight="6dip"
android:layout_marginTop="6dip"
android:layout_marginBottom="6dip"
android:layout_weight="1">
<TextView android:id="#+android:id/title"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:singleLine="true"
android:textAppearance="?android:attr/textAppearanceLarge"
android:ellipsize="marquee"
android:fadingEdge="horizontal" />
<TextView android:id="#+android:id/summary"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_below="#android:id/title"
android:layout_alignLeft="#android:id/title"
android:textAppearance="?android:attr/textAppearanceSmall"
android:maxLines="2" />
<ImageButton android:id="#+id/pref_delete_station" android:layout_width="wrap_content" android:layout_height="wrap_content" android:src="#drawable/ic_trash_can" android:layout_alignParentRight="true" android:background="#null"></ImageButton>
</RelativeLayout>
<!-- Preference should place its actual preference widget here. -->
<LinearLayout android:id="#+android:id/widget_frame"
android:layout_width="wrap_content"
android:layout_height="fill_parent"
android:gravity="center_vertical"
android:orientation="vertical" />
</LinearLayout>
Related part of my PreferenceActivity:
DialogPreference diaPref;
for (Station mStation : sList) {
diaPref = new StationEditor(this.getPreferenceScreen().getContext(), null, this, mStation);
diaPref.setLayoutResource(R.layout.custom_pref_row);
diaPref.setTitle(mStation.getName());
diaPref.setKey(STATION_PREFIX + mStation.getId());
// add new preference
stationTypesCategory.addPreference(diaPref);
}
You can extend DialogPreference and override the onBindDialogView(View view). Inside this method you can do:
#Override
protected void onBindDialogView(View view) {
((ImageButton) view.findViewById(R.id.pref_delete_station)).setOnClickListener(new OnClickListener() {
#Override
public void onClick(View v) {
// TODO Auto-generated method stub
}
});
super.onBindDialogView(view);
}
Your sublcass of DialogPreference can hold any state/value related to the item it represents.
Take a look at this question about general guidelines to extend DialogPreference.
Hope this helps!
OK, Chopin got me thinking in a different direction. I did not realize that the Preference object is also responsible for how its selector appears in a Preference screen.
The setLayoutResouce() function sets the resource for the Dialog itself not the row seen in a Preference screen. This was confusing and I was incorrectly trying to use this in the preference screen to adjust the selector layout there.
The solution is to override onCreateView and return a custom layout there. To me this is counterintuitive because that method usually controls the final view in most other situations.
I alraedy subclassed my Preference (DialogPreference) so all I had to do was add the following...
#Override
protected View onCreateView (ViewGroup parent) {
LayoutInflater inflater = (LayoutInflater) getContext().getSystemService(Context.LAYOUT_INFLATER_SERVICE);
View customRow = inflater.inflate(R.layout.preferences_station_list_row, null);
((ImageButton) customRow.findViewById(R.id.pref_delete_station)).setOnClickListener(new OnClickListener() {
#Override
public void onClick(View v) {
Log.i("c","clicked");
}
});
customRow.setOnClickListener(new OnClickListener() {
#Override
public void onClick(View v) {
showDialog(null);
}
});
customRow.setClickable(true);
return customRow;
}
One problem I ran into was that at first the row itself was no longer clickable but the button was. I had to add a listener on the whole view and manually call ShowDialog(). The only thing missing now is that when clicked from the Preference screen the item no longer shows a highlight. Any idea what styles I should apply so the list shows the highlight like it normally does?
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:
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>
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.
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;
}
});
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