I'm trying to learn how to develop Android apps. I followed a video tutorial on YouTube, and it ended by adding a simple App Settings screen to the application.
However, there's one point that bothers me: when I press the back button on my phone's navigation bar, the changed settings aren't applied.
I have tried searching on Google, but none of the solutions I found have worked. The fact that I don't yet understand 100% of what's happening on the proposed solutions may also contribute to my difficulty on solving this one problem.
The behavior I expect from the app is that when I press the back button on the navigation bar, the changed settings should be applied.
For instance, I have a setting for dark background, which is controlled by a checkbox. The current behavior is: I check the setting for dark background. When I press the back button on the navigation bar, the setting isn't applied (I do have a method that loads the preferences on my MainActivity). What I want to happen is when I press the back button, the dark background is applied in this case.
From what I understand, I believe that overriding onBackPressed should do the trick, but I don't know what should be executed in order to properly apply the settings.
Here are the class and layout of my PreferenceScreen. Regarding the strings on the XML, they aren't actually hard-coded. I just copied the English values here to show the text that should appear on the interface.
public class AppPreferences extends AppCompatActivity
{
#Override
protected void onCreate(Bundle savedInstanceState)
{
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_note_detail);
FragmentManager fragmentManager = getFragmentManager();
FragmentTransaction fragmentTransaction = fragmentManager.beginTransaction();
SettingsFragment settingsFragment = new SettingsFragment();
fragmentTransaction.add(android.R.id.content, settingsFragment, "SETTINGS_FRAGMENT");
fragmentTransaction.commit();
}
public static class SettingsFragment extends PreferenceFragment
{
#Override
public void onCreate(Bundle savedInstanceState)
{
super.onCreate(savedInstanceState);
addPreferencesFromResource(R.xml.app_preferences);
}
}
}
<?xml version="1.0" encoding="utf-8"?>
<PreferenceScreen xmlns:android="http://schemas.android.com/apk/res/android">
<PreferenceCategory
android:title="General">
<EditTextPreference
android:title="Notebook"
android:summary="The title that will be used on the main action bar."
android:key="title"
android:defaultValue="Notebook" />
</PreferenceCategory>
<PreferenceCategory
android:title="Color">
<CheckBoxPreference
android:title="Dark Background"
android:summary="Is the main background color dark?"
android:key="background_color"
android:defaultValue="false" />
</PreferenceCategory>
</PreferenceScreen>
You will need to use
public class AppPreferences extends AppCompatActivity
{
#Override
protected void onCreate(Bundle savedInstanceState)
{
super.onCreate(savedInstanceState);
//setContentView(R.layout.activity_note_detail);
FragmentManager fragmentManager = getFragmentManager();
FragmentTransaction fragmentTransaction = fragmentManager.beginTransaction();
SettingsFragment settingsFragment = new SettingsFragment();
fragmentTransaction.add(android.R.id.content, settingsFragment, "SETTINGS_FRAGMENT");
fragmentTransaction.commit();
}
public static class SettingsFragment extends PreferenceFragment
{
#Override
public void onCreate(Bundle savedInstanceState)
{
super.onCreate(savedInstanceState);
addPreferencesFromResource(R.xml.app_preferences);
Preference preference = findPreference("background_color");
preference.setOnPreferenceChangeListener(new Preference.OnPreferenceChangeListener() {
#Override
public boolean onPreferenceChange(Preference preference, Object newValue) {
//do your action here
return false;
}
});
}
}
}
Or from other activity:
PreferenceManager.setDefaultValues(this, R.xml.your_setting_xml, false);
SharedPreferences settings = PreferenceManager.getDefaultSharedPreferences(this);
if (settings.getBoolean("background_color", true)) {
//do your action here
.........
Refer to this question also, (it has similar use case):
Checkbox Preference and Checking if its enabled or disable
Like #Chisko said, there isn't enough code in your question for us to be able to figure out your end goal - although I'm guessing you are wanting some form of persistent storage for your app to be able to save your app preferences. For this, you will want to use something in Android called SharedPreferences. This allows you to save simple data types to be accessed later.
Give that link a read, and then try saving/loading one simple piece of data. You'll want to load it from SharedPreferences on starting the activity (you can specify a default value if it hasn't been saved yet) and then you'll want to save the data in onBackPressed() as you said.
Best of luck and if you run into any issues, just comment here.
#Abdulhamid Dhaiban correctly points it out.
I'll add to your suggestion about overriding the onBackPressed() method.
If your "Up" button (top left <-) provides the correct result, then you can set the Back button to behave like the Up button by just adding the following code:
#Override
public void onBackPressed() {
super.onBackPressed();
NavUtils.navigateUpFromSameTask(this);
}
Hope it helps!
Related
I'm using SettingsActivity as preference activity for my application like so:
public class SettingsActivity extends AppCompatActivity {
#Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
if (PreferenceManager.getDefaultSharedPreferences(this).getBoolean(getString(R.string.dark_theme_key), false)) {
setTheme(R.style.AppThemeDark);
} else {
setTheme(R.style.AppThemeLight);
}
if (savedInstanceState == null) {
getFragmentManager().beginTransaction()
.replace(android.R.id.content, new SettingsFragment())
.commit();
}
}
public static class SettingsFragment extends PreferenceFragment {
#Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
addPreferencesFromResource(R.xml.app_preferences);
...
}
}
}
If I use getActivity().recreate(); (I have a SwitchPreference for dark and light theme. Dark theme is Theme.AppCompat and light is same but with .Light. If I change the theme I use recreate.) or change orientation of the screen (Resulting in onCreate of course), I get a weird problem when I'm on my light theme.
The problem is as follows -after a recreate, the text content of the EditTextPreference's turn white instead of black.
If I simply ignore savedInstanceState == null and just create a new fragment every time on SettingsActivity.onCreate, I lose the "screen state" (for example if I was scrolled all the way down before, after recreate Im scrolled back to the top. If I had a EditTextPreference open before, after recreate its closed).
I'm not sure how to restore a fragment screen state in case of recreation, or why my problem even occurs. Would appreciate help.
I want my app to show the side navigation drawer as soon as the main activity is created.
My code works fine - user launches app and gets the open drawer - but I'd like to actually see the side drawer sliding from the left; instead, I find the drawer fully opened.
At what point should I call openDrawer()?
Have tried calling from:
main activity OnCreate;
similar points in the fragment hosted by the drawer.
I could try OnPrepareOptionsMenu, but I think it gets called more than once during the activity lifecycle. I also tried OnStart() and I fear my options are over.
Any idea? I'm sure this is pretty simple but I can't figure out.
Edit: I realize I wasn't so clear with my first exposition of the question (#Biu). I'm talking about a purely graphical issue here. The point is:
I have something to happen at startup; in my case we're speaking about the nav drawer sliding into the main screen, but it could be any animation I think;
In my case, one could just call:
protected void OnCreate(Bundle b) {
...
DrawerLayout.openDrawer()
}
The above solution works well. The issue I'm talking about is graphical; with the above code you launch the app and find the main activity covered with an already-opened drawer. Instead, I'd like the user to have clue of what is happing, to see where the panel came from; in other words, to see the opening animation.
So my question is: when should I call openDrawer()? The main Activity onCreate isn't quite right, because the animation ends before the user gets to see something on screen.
I thought that the wish of having something start when all is loaded would be more common.
#benjosantony suggests that you should open your drawer at onResume, however it's not guaranteed that the activity will be visible at that time:
onResume is not the best indicator that your
activity is visible to the user; Use onWindowFocusChanged(boolean) to know for certain
that your activity is visible to the user
You'd think that you can just use onWindowFocusChanged and be done, but you can't. There's still the transition animation which breaks (at least for me) the drawer's animation..
For API 21+:
There's onEnterAnimationComplete where you can open your drawer and see the animation properly. However 21+ is a requirement that's just too big..
For lower APIs:
The only possible way I can think of is removing the activity's animation with a theme adjustment:
<style name="AppTheme" parent="Theme.AppCompat.Light.DarkActionBar">
<item name="android:windowAnimationStyle">#null</item>
</style>
And opening the drawer like so:
private static final String DRAWER_STATE = "mDrawerOpened";
private DrawerLayout mDrawer;
private ListView mDrawerList;
private boolean mDrawerOpened;
#Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
mDrawer = (DrawerLayout) findViewById(R.id.drawer_layout);
mDrawerList = (ListView) findViewById(R.id.left_drawer);
}
#Override
public void onWindowFocusChanged(boolean hasFocus) {
super.onWindowFocusChanged(hasFocus);
if (!mDrawerOpened && hasFocus) {
mDrawer.openDrawer(mDrawerList);
mDrawerOpened = true;
}
}
#Override
protected void onSaveInstanceState(#NonNull Bundle outState) {
super.onSaveInstanceState(outState);
outState.putBoolean(DRAWER_STATE, mDrawerOpened);
}
#Override
protected void onRestoreInstanceState(#NonNull Bundle savedInstanceState) {
super.onRestoreInstanceState(savedInstanceState);
mDrawerOpened = savedInstanceState.getBoolean(DRAWER_STATE);
}
This will animate the drawer only when the activity is started.
The boolean value is saved when your activity is destroyed abnormally, e.g. rotation or need for system resources.
If you don't like setting the instanceState you can use SharedPreferences as #Biu suggested, however IMO that wouldn't be the proper solution as android already provides tools for that, there's no need to re-invent the bike.
You could use this hack by using SharedPreferences
boolean firstTime = true;
SharedPreferences pref = PreferenceManager.getDefaultSharedPreferences(this);
public void onCreate(Bundle bundle)
{
if (pref.getBoolean("firstTime", true) == true)
{
drawer.openDrawer(yourDrawer);
firstTime = false;
pref.editor().putBoolean("firstTime", firstTime).apply();
}
}
Activity
The foreground lifetime of an activity happens between a call to onResume() until a corresponding call to onPause(). During this time the activity is in front of all other activities and interacting with the user.
Thus I think onResume is the best place to open your drawer.
I've implemented my preferences like shown in the official guidelines.
I have a PreferenceActivity which creates the PreferenceFragment like this:
#Override
protected void onCreate(Bundle savedInstanceState)
{
super.onCreate(savedInstanceState);
Bundle extras = getIntent().getExtras();
if (extras != null)
{
Bundle bundle = new Bundle();
_widgetID = extras.getInt(GlobalSettings.EXTRA_WIDGET_ID);
bundle.putInt(GlobalSettings.EXTRA_WIDGET_ID, _widgetID);
WidgetSettingsFragment fragment = new WidgetSettingsFragment();
fragment.setArguments(bundle);
getFragmentManager().beginTransaction().replace(android.R.id.content,
fragment).commit();
}
}
The PreferenceFragment loads the preferences from the resources and they contain a preference subscreen like this:
<PreferenceScreen xmlns:android="http://schemas.android.com/apk/res/android">
<!-- opens a subscreen of settings -->
<PreferenceScreen
android:key="button_voicemail_category_key"
android:title="#string/voicemail"
android:persistent="false">
<ListPreference
android:key="button_voicemail_provider_key"
android:title="#string/voicemail_provider" ... />
<!-- opens another nested subscreen -->
<PreferenceScreen
android:key="button_voicemail_setting_key"
android:title="#string/voicemail_settings"
android:persistent="false">
...
</PreferenceScreen>
<RingtonePreference
android:key="button_voicemail_ringtone_key"
android:title="#string/voicemail_ringtone_title"
android:ringtoneType="notification" ... />
...
</PreferenceScreen>
...
</PreferenceScreen>
This works well so far, but now I'd like to have an up-Button in the actionBar when the preferences subscreen is shown. Any idea how to accomplish that?
I have tried to set setDisplayHomeAsUpEnabled(true) in my activity but then the up-Button is only shown in the main preferences (where it should not) and not in the subscreen.
I'm wondering that even in the official docs the subscreen is shown without an active up-Button:
Link to the docs: Settings
Any help is really welcome
I finally got it to work :D. It's quite hacky but it works.
The problem is, that using subscreens in xml-layouts results in some 'code magic'.
A new activity/dialog is started for the subscreen and you don't have direct access to it.
To get access to the actionbar and the OnClickListener of the home/up-button you need to get a reference to your PreferenceScreen and get its parent Dialog in order to access the actionbar and its home/up button.
This is how it is done inside my PreferenceFragment:
#Override
public void onCreate(Bundle savedInstanceState)
{
...
final PreferenceScreen preferenceScreen = (PreferenceScreen) findPreference(getString(R.string.keyPrefScreenDynamicWidgetDetails));
preferenceScreen.setOnPreferenceClickListener(new OnPreferenceClickListener()
{
public boolean onPreferenceClick(Preference preference)
{
preferenceScreen.getDialog().getActionBar().setDisplayHomeAsUpEnabled(true);
final Dialog dialog = preferenceScreen.getDialog();
View homeBtn = dialog.findViewById(android.R.id.home);
if (homeBtn != null)
{
OnClickListener dismissDialogClickListener = new OnClickListener()
{
#Override
public void onClick(View v)
{
dialog.dismiss();
}
};
// Prepare yourselves for some hacky programming
ViewParent homeBtnContainer = homeBtn.getParent();
// The home button is an ImageView inside a FrameLayout
if (homeBtnContainer instanceof FrameLayout) {
ViewGroup containerParent = (ViewGroup) homeBtnContainer.getParent();
if (containerParent instanceof LinearLayout) {
// This view also contains the title text, set the whole view as clickable
((LinearLayout) containerParent).setOnClickListener(dismissDialogClickListener);
} else {
// Just set it on the home button
((FrameLayout) homeBtnContainer).setOnClickListener(dismissDialogClickListener);
}
} else {
// The 'If all else fails' default case
homeBtn.setOnClickListener(dismissDialogClickListener);
}
}
return true;
}
});
...
}
Following link gave me the final hints and code to solve my problem:
Action Bar Home Button not functional with nested PreferenceScreen
I do this per the Android docs in the "Supporting older versions with preference headers" section http://developer.android.com/guide/topics/ui/settings.html#BackCompatHeaders. Using the legacy PreferenceActivity, you specify a Preference in the xml that launches an intent to the same preference activity class. The activity checks the intent action and determines if it is nested or not (to show the up button) and which preference xml to inflate in the screen.
Of course, I intend to support older devices as well. I have found that the PreferenceFragment is only useful for large tablets that use preference headers.
To reuse preferences between phones and tablets I came up with this solution https://stackoverflow.com/a/20806812/1139784
To enable the up action do the following:
getSupportActionBar().setDisplayHomeAsUpEnabled(true);
getSupportActionBar().setDisplayUseLogoEnabled(true);
this will give you the icon.
then add
#Override
public boolean onOptionsItemSelected(MenuItem item) {
switch (item.getItemId()) {
// Respond to the action bar's Up/Home button
case android.R.id.home:
finish();
return true;
}
return super.onOptionsItemSelected(item);
}
you can alter this to go where you need to. As another option you can use the navigateUpTo(Intent intent) and the onSupportNavigateUpTo(Intent intent) methods and specify the intent you want to return to.
I've seen quite a few questions on SO about Fragments and I still can't seem to figure out if what I want to do is possible, and more so if my design pattern is just flawed and I need to re-work the entire process. Basically, like most questions that have been asked, I have an ActionBar with NavigationTabs (using ActionBarSherlock), then within each Tab there is a FragementActivity and then the FragmentActivities push new Fragments when a row is selected (I'm trying to re-create an iOS Project in Android and it's just a basic Navigation based app with some tabs that can drill down into specific information). When I click the back button on the phone the previous Fragment is loaded but the Fragment re-creates itself (so the WebServices are called again for each view) and this isn't needed since the information won't change in a previous view when going backwards. So basically what I want to figure out is how do I setup my Fragments so that when I push the back button on the phone, the previous Fragment is just pulled up with the previous items already created. Below is my current code :
//This is from my FragmentActivity Class that contains the ActionBar and Tab Selection Control
#Override
public void onTabSelected(ActionBar.Tab tab, FragmentTransaction ft) {
// TODO Auto-generated method stub
int selectedTab = tab.getPosition();
if (selectedTab == 0) {
SalesMainScreen salesScreen = new SalesMainScreen();
ft.replace(R.id.content, salesScreen);
}
else if (selectedTab == 1) {
ClientMainScreen clientScreen = new ClientMainScreen();
ft.replace(R.id.content, clientScreen);
}.....
//This is within the ClientMainScreen Fragment Class, which handles moving to the Detail Fragment
row.setOnClickListener(new View.OnClickListener() {
#Override
public void onClick(View view) {
//Do something if Row is clicked
try{
String selectedClientName = clientObject.getString("ClientName");
String selectedClientID = clientObject.getString("ClientID");
String selectedValue = clientObject.getString("ClientValue");
transaction = getFragmentManager().beginTransaction();
ClientDetailScreen detailScreen = new ClientDetailScreen();
detailScreen.clientID = selectedClientID;
detailScreen.clientName = selectedClientName;
detailScreen.clientValue = selectedValue;
int currentID = ((ViewGroup)getView().getParent()).getId();
transaction.replace(currentID,detailScreen);
transaction.addToBackStack(null);
transaction.commit();
}
catch (Exception e) {
e.printStackTrace();
}
}
});....
//And then this is the Client Detail Fragment, with the method being called to Call the Web Service and create thew (since what is displayed on this screen is dependent on what is found in the Web Service
#Override
public View onCreateView(LayoutInflater inflater, ViewGroup group, Bundle saved) {
return inflater.inflate(R.layout.clientdetailscreen, group, false);
}
#Override
public void onActivityCreated (Bundle savedInstanceState) {
super.onActivityCreated(savedInstanceState);
//Setup Preferences File Link
this.preferences = PreferenceManager.getDefaultSharedPreferences(getActivity());
//initialize the table object
mainTable = (TableLayout)getActivity().findViewById(R.id.mainTable);
//setup the detail table
setupRelatedClientSection();
}
The Client Detail Screen can then drill down one more time, using the same method as the Client Main Screen but when I go back from that new screen to the Detail Screen the seuptRelatedClientSection() method is called again and so the entire Fragment is rebuilt when really I just want to pull up a saved version of that screen. Is this possible with my current setup, or did I approach this the wrong way?
Try using fragementTransaction.add instead of replace
I believe that you are looking for show() and hide().
I think you can still add them to the backstack.
transaction.hide(currentFragment);
transaction.show(detailScreen);
transaction.addToBackStack(null);
transaction.commit();
I didnt have my code to look at but i believe this is how it would go... Try it out unless someone else has a better way.
I have not tried the backstack with show() hide() but i believe that it takes the changes that are made before the transactions commit and will undo them if the back button is pressed. Please get back to me on this cause i am interested to know.
You also have to make sure that the detail fragment is created before you call this. Since it is based on the click of someitem then you should probably create the details fragment every time you click to make sure the correct details fragment is created.
I'm posting this answer for people who may refer this question in future.
Following code will demonstrate how to open FragmentB from FragmentA and going back to FragmentA from FragmentB (without refreshing FragmentA) by pressing back button.
public class FragmentA extends Fragment{
...
void openFragmentB(){
FragmentManager fragmentManager =
getActivity().getSupportFragmentManager();
FragmentB fragmentB = FragmentB.newInstance();
if (fragmentB.isAdded()) {
return;
} else {
fragmentManager.
beginTransaction().
add(R.id.mainContainer,fragmentB).
addToBackStack(FragmentB.TAG).
commit();
}
}
}
public class FragmentB extends Fragment{
public static final String TAG =
FragmentB.class.getSimpleName();
...
public static FragmentB newInstance(){
FragmentB fragmentB = new FragmentB();
return fragmentB;
}
#Override
public void onResume() {
super.onResume();
// add this piece of code in onResume method
this.getView().setFocusableInTouchMode(true);
this.getView().requestFocus();
}
}
In your MainActivity override onBackPressed()
class MainActivity extends Activity{
...
#Override
public void onBackPressed() {
if (getFragmentManager().getBackStackEntryCount() > 0) {
getFragmentManager().popBackStack();
} else {
super.onBackPressed();
}
}
}
You're right, there has been a number of previous questions / documentation on the topic ;)
The documentation on Fragments, specifically the section about Transactions and Saving State, will guide you to the answer.
http://developer.android.com/guide/components/fragments.html#Transactions
http://developer.android.com/guide/components/activities.html#SavingActivityState
Android - Fragment onActivityResult avoid reloading
Fragments can have support for onSaveInstanceState but not onRestoreInstanceState, so if you want to save a reference to the table views, save them to the Bundle and you can access the saved view in your onActivityCreated method. You could also use the Fragments back stack.
This guide/tutorial has very detailed instructions/examples on the back stack and retaining fragment state.
Good luck
I am in the process of making a honeycomb project/fork backwards compatible with 1.6+.
Based on the documentation provided by Google/Android I decided to build off all my fragments off DialogFragments which worked great for honeycomb...it gives me the flexibility to put anything as a dialog or 'full screen' element.
I've now incorporated the compatibility kit and moved my imports and method calls over to that. Now that I'm on 2.3 I am trying to launch an identical intent but I receive this issue:
java.lang.IllegalStateException: DialogFragment can not be attached to a container view
The documentation for DialogFragment suggests that it can perform as Fragment when you don't desire the dialog/popup functionality.
I managed to fix this properly in DialogFragment.java of the Compatibility Package:
Change line 74:
boolean mShowsDialog = false;
Comment out line 232: //mShowsDialog = mContainerId == 0;
Then change the two show methods to this:
public void show(FragmentManager manager, String tag) {
this.setShowsDialog(true);
FragmentTransaction ft = manager.beginTransaction();
ft.add(this, tag);
ft.commit();
}
// JavaDoc removed
public int show(FragmentTransaction transaction, String tag) {
this.setShowsDialog(true);
transaction.add(this, tag);
mRemoved = false;
mBackStackId = transaction.commit();
return mBackStackId;
}
Basically, they did write in support but the toggle to switch between dialog/embedded doesn't work.
So here we default to embedded, and then set to show as a dialog just before we show it.
You can use the android.support.v4.app.DialogFragment version, please check here
I've had the same problem. I never found a "correct" solution, but you can do the same thing by setting the theme of the Dialog in OnCreateDialog(). By setting the theme to android.R.style.Theme_Holo_DialogWhenLarge the dialog will be shown as a dialog on large and xlarge screens, while it'll be shown as a full screen window on small and normal screens.
#Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setStyle(STYLE_NO_TITLE, android.R.style.Theme_Holo_DialogWhenLarge);
}
I am using a DialogFragment child class and doing this trick in the onCreate() works. I call setShowsDialog() before onCreate() is called (in the onAttachFragment() of my Activity)
public class DialogFragmentHosted extends DialogFragment {
#Override
public void onCreate(Bundle savedInstanceState) {
boolean forceShowDialog = savedInstanceState==null;
boolean showsDialog = getShowsDialog();
super.onCreate(savedInstanceState);
if (forceShowDialog )
setShowsDialog(showsDialog);
}
}
Did you check the application note? It shows a recommended way of embedding a dialog, and I've verified this works on 2.2.1.
http://developer.android.com/reference/android/app/DialogFragment.html#DialogOrEmbed
My fragment layout had to change to conform but it was quick and easy. It's more natural to be able to define the dialog fragment in XML and expect it to be embedded without any extra work (as the above changes to Compat API would support); and to only expect modal behavior when called through show(). I suppose that isn't the current behavior.