android: How to make settings navigation like native settings app - android

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

Related

Cannot do Alpha Animation with LinearLayout on Android

So I was trying to make a ActionButton like Trello have (click on a button, a black overlay apears, and other buttons appear on the screen).
But I had one weird problem: I cannot make the alpha animation work.
I tryed in two ways:
private void initializeComponent(final Context ctx, final AttributeSet attrs) {
blackOverlay.setAlpha(0.0f);
blackOverlay.setVisibility(View.VISIBLE);
blackOverlay.setClickable(true);
actionButton.setOnClickListener(new OnClickListener() {
#Override
public void onClick(View v) {
final boolean closed = blackOverlay.getAlpha() == 0.0f;
blackOverlay.animate().alpha(closed ? 1f : 0f).setDuration(500).setListener(null);
});
}
So in this way, the view blackOverlay is always visible and the animation should work. But it doesn't. Clicking the button does not make any difference on alpha. The only change is that toggling the button makes the non-overlay content work/stop work (as it should since blackOverlay is clickable).
So I tryed another approach: not setting the alpha to 0.0f so the overlay will start visible to the user. Basically commenting the first line of initializeComponent method.
private void initializeComponent(final Context ctx, final AttributeSet attrs) {
//blackOverlay.setAlpha(0.0f);
blackOverlay.setVisibility(View.VISIBLE);
blackOverlay.setClickable(true);
actionButton.setOnClickListener(new OnClickListener() {
#Override
public void onClick(View v) {
final boolean closed = blackOverlay.getAlpha() == 0.0f;
blackOverlay.animate().alpha(closed ? 1f : 0f).setDuration(500).setListener(null);
}
});
}
In this way, the button only toggles the alpha, it doesn't animate it.
Any ideas? I've already tryed a lot of solutions here on StackOverflow.
Thanks!
This is my xml layout:
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:orientation="vertical" android:layout_width="match_parent"
android:layout_height="match_parent">
<LinearLayout
android:orientation="horizontal"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:id="#+id/blackoverlay"
android:background="#color/black_overlay">
</LinearLayout>
<ImageButton
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:id="#+id/action_button"
android:layout_gravity="bottom"
android:src="#drawable/plus"
android:layout_margin="50dp"
android:layout_alignParentBottom="true"
android:layout_alignParentEnd="true" />
</RelativeLayout>
OK so I did a MCVE as karaokyo said but the fun thing was: It worked on MCVE.
So I started looking at my entire code and I found that I had a setOnClickListener for a Button that had the same name as action_button. So I just changed the IDs to not be the same and everything is working now.
It seens it was my bad, I'm stupid lol
So for anyone that is reading this: Try to create a MCVE from scratch (new project even) and see if it works. It maybe be something else non-related interfering in your code.

How to add a sliding drawer a MapView/View that's created programmatically

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.

How to attach OnClickListener to a button in a custom preference layout?

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?

Coupling Android layout to its logic outside of the owner Activity

I'm facing some complexities when developing a medium-complex Android application. I'm searching for information about the possibility of using code-behind-like techniques for easier maintanability of Android software.
Currently (please highlight anything wrong), I have found that in order to make a multi-step wizard with extra dialogs (eg. dialogs that are not part of the main sequence) I need to code a single XML layout file with a single ViewFlipper containing each subview as child node. Today I discovered how to navigate across views more than forward/backward (viewFlipper.setDisplayedChild(i)), giving access to extra views.
Now all the Java code is contained within the main Activity class, which is beginning to look bad. As an experienced .NET developer I have learned how to use custom controls to wrap both layout and business logic inside modules.
I know that in Android I can define a view programmatically as an independent class and add it to the main layout programmatically, however I want to know if it's possible in Android to define a layout by XML (for easier WYSIWYG creation/editing) and define all the code within a dedicated class, with initialization logic, button callbacks, async tasks, etc.
I'm not sure if it's feasible or there is a good compromise that can be achieved.
I have read this question without clearing my doubts.
Thank you.
Code examples:
An extract of the layout file (I expect 4 wizard steps, a help view and an EULA view)
<?xml version="1.0" encoding="utf-8"?>
<ViewFlipper xmlns:android="http://schemas.android.com/apk/res/android"
android:id="#+id/view_phone"
style="#android:style/Theme.Light.NoTitleBar"
android:layout_width="fill_parent"
android:layout_height="fill_parent" >
<!-- First screen/welcome -->
<LinearLayout
android:id="#+id/view_phone_screen1"
style="#android:style/Theme.Light.NoTitleBar"
android:layout_width="fill_parent"
android:layout_height="fill_parent"
android:orientation="vertical"
android:weightSum="100" >
<TextView
android:id="#+id/view_phone_screen1_lblChooseProvider"
android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:gravity="center_vertical|center_horizontal"
android:text="#string/view_phone_lblChooseProvider_1ststep"
android:textAppearance="?android:attr/textAppearanceLarge" />
<ImageButton
android:id="#+id/view_phone_btnFrecciarossa"
android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:clickable="true"
android:contentDescription="#string/provider_FRECCIAROSSA"
android:gravity="center_vertical|clip_vertical"
android:padding="10dp"
android:src="#drawable/logo_frecciarossa"
android:tag="#+id/provider_FRECCIAROSSA" />
<ImageButton
android:id="#+id/view_phone_btnItalo"
android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:clickable="true"
android:contentDescription="#string/provider_ITALO"
android:gravity="center_vertical|clip_vertical"
android:padding="10dp"
android:src="#drawable/logo_italo"
android:tag="#+id/provider_ITALO" />
</LinearLayout>
<!-- Second screen - will need to do some asynchronous task -->
<RelativeLayout
android:id="#+id/view_phone_screen2"
android:layout_width="fill_parent"
android:layout_height="fill_parent" >
<TextView
android:id="#+id/view_phone_screen2_lblConnectingToWifi"
android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:gravity="center_vertical|center_horizontal"
android:text="#string/view_phone_lblConnectToWifi_2ndstep"
android:textAppearance="?android:attr/textAppearanceLarge" />
<TextView
android:id="#+id/view_phone_step2_lblConnectedToWifi"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_below="#+id/imageView1"
android:layout_centerHorizontal="true"
android:layout_marginTop="58dp"
android:text="#string/view_phone_step2_connectingToWifi"
android:textAppearance="?android:attr/textAppearanceLarge" />
<TextView
android:id="#+id/view_phone_step2_lblPhoneNumber"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignLeft="#+id/editText1"
android:layout_below="#+id/view_phone_step2_lblConnectedToWifi"
android:layout_marginTop="51dp"
android:text="#string/view_phone_step2_msgInputPhoneNumber"
android:textAppearance="?android:attr/textAppearanceMedium" />
<TextView
android:id="#+id/view_phone_step2_lblUnableDetectPhoneNumber"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentRight="true"
android:text="#string/view_phone_step2_msgUnableDetectPhoneNumber"
android:textAppearance="?android:attr/textAppearanceSmall"
android:visibility="invisible" />
<Button
android:id="#+id/view_phone_screen2_backward"
style="#style/buttonBackward" />
<Button
android:id="#+id/view_phone_screen2_forward"
style="#style/buttonForward_disabled"
android:enabled="false" />
<EditText
android:id="#+id/view_phone_step2_txtPhoneNumber"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentLeft="true"
android:layout_alignRight="#+id/view_phone_step2_lblPhoneNumber"
android:layout_below="#+id/view_phone_step2_lblPhoneNumber"
android:inputType="phone"
android:singleLine="true" >
<requestFocus />
</EditText>
</RelativeLayout>
</ViewFlipper>
Code example from Activity (expect to implement ALL the logic of 4+2 step wizard)
public class MyActivity extends Activity {
/** Called when the activity is first created. */
private final static String LOG_TAG = "LOG_TAG";
private int stepNumber;
#Override
public void onCreate(Bundle savedInstanceState) {
this.requestWindowFeature(Window.FEATURE_NO_TITLE);
this.setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_PORTRAIT);
super.onCreate(savedInstanceState);
this.stepNumber=1;
setContentView(R.layout.view_phone);
//This class wraps the click for the two buttons
ProviderSelectionListener providerSelectionListener = new ProviderSelectionListener(this);
this.findViewById(R.id.view_phone_btnFrecciarossa).setOnClickListener(providerSelectionListener);
this.findViewById(R.id.view_phone_btnItalo).setOnClickListener(providerSelectionListener);
}
#Override
protected void onPause() {
super.onPause();
try {
if (MyApplication.getPlatformManager() != null)
MyApplication.getPlatformManager().onApplicationPause();
} catch (MyCustomException e) {
// WTF (Worse Than Failure!)
Log.e(LOG_TAG, super.getString(R.string.zf_error_unknown_error_pauseactivity), e);
e.printStackTrace();
}
}
#Override
protected void onResume() {
super.onResume();
try {
if (MyApplication.getPlatformManager() != null)
MyApplication.getPlatformManager().onApplicationResume();
} catch (MyCustomException e) {
// WTF (Worse Than Failure!)
Log.e(LOG_TAG, super.getString(R.string.zf_error_unknown_error_pauseactivity), e);
e.printStackTrace();
}
}
/*
* SLIDE INIZIO
*/
protected void slideNext() {
ViewFlipper vf = (ViewFlipper) findViewById(R.id.view_phone);
vf.setOutAnimation(getApplicationContext(), R.anim.slide_out_left);
vf.setInAnimation(getApplicationContext(), R.anim.slide_in_right);
vf.showNext();
}
protected void slidePrevious() {
ViewFlipper vf = (ViewFlipper) findViewById(R.id.view_phone);
vf.setOutAnimation(getApplicationContext(), R.anim.slide_out_right);
vf.setInAnimation(getApplicationContext(), R.anim.slide_in_left);
vf.showPrevious();
}
/*
* SLIDE FINE
*/
/*
* STEP 1 INIZIO
*/
public void completeStep1(ISmsWifiProvider provider) {
if (provider == null) {
Log.e(LOG_TAG, "Provider nullo");
return;
}
MyApplication.setAuthenticationProvider(provider);
slideNext();
initializeStep2();
}
public void returnToStep1() {
MyApplication.setAuthenticationProvider(null);
slidePrevious();
}
/*
* STEP 1 FINE
*/
/*
* STEP 2 INIZIO
*/
private void initializeStep2() {
// Event handler
Button backButton = (Button) findViewById(R.id.view_phone_screen2_backward), fwButton = (Button) findViewById(R.id.view_phone_screen2_forward);
fwButton.setEnabled(false);
backButton.setOnClickListener(new OnClickListener() {
#Override
public void onClick(View v) {
returnToStep1();
}
});
}
/*
* STEP 2 FINE
*/
#Override
public void onBackPressed() {
// This will be called either automatically for you on 2.0
// or later, or by the code above on earlier versions of the
// platform.
return;
}
}
I want to know if it's possible in Android to define a layout by XML (for easier WYSIWYG creation/editing) and define all the code within a dedicated class, with initialization logic, button callbacks, async tasks, etc.
Yes. It is one of the techniques for creating a custom View. For example, I have a custom ColorMixer widget in this project, which can be used directly in an activity, or in a dialog, or in a custom preference.
I agree with your tactical decision to implement a wizard via a ViewFlipper -- see this answer to another StackOverflow question for "Murphy's Theory of the Activity".
I suspect that the right answer, longer term, is for somebody (who might be me) to come up with a Fragment-based wizard pattern, as that gives you the decoupling you desire.

Button Onclick Listener in included layouts

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.

Categories

Resources