Adding toolbar to PreferenceActivity via AppCompatPreferenceActivity causes overlapping - android

I had gone through How to add toolbars to AppCompatPreferenceActivity?
I'm using appcompat-v7:23.0.1 and support-v4:23.0.1
Before using AppCompayPreferenceActivity, my PreferenceActivity looks the following without toolbar.
Without Toolbar
public class JStockPreferenceActivity extends PreferenceActivity {
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
if (savedInstanceState == null) {
// Display the fragment as the main content.
JStockPreferenceFragment jStockPreferenceFragment = JStockPreferenceFragment.newInstance();
jStockPreferenceFragment.setArguments(this.getIntent().getExtras());
this.getFragmentManager().beginTransaction().replace(android.R.id.content, jStockPreferenceFragment).commit();
}
}
After using AppCompayPreferenceActivity, with some Toolbar initialization code, the outcome looks as the following
The outcome isn't as expected :-
The Toolbar overlaps with PreferenceFragment
When touching on Toolbar, the event will be consumed by PreferenceFragment. This means, when you touch on toolbar, "Table header" preference will be touched instead.
With Toolbar
public class JStockPreferenceActivity extends AppCompatPreferenceActivity {
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
if (savedInstanceState == null) {
// Display the fragment as the main content.
JStockPreferenceFragment jStockPreferenceFragment = JStockPreferenceFragment.newInstance();
jStockPreferenceFragment.setArguments(this.getIntent().getExtras());
this.getFragmentManager().beginTransaction().replace(android.R.id.content, jStockPreferenceFragment).commit();
}
initToolbar();
}
private void initToolbar() {
Toolbar toolbar;
if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.ICE_CREAM_SANDWICH) {
ViewGroup root = (ViewGroup) findViewById(android.R.id.list).getParent().getParent().getParent();
toolbar = (Toolbar) LayoutInflater.from(this).inflate(R.layout.toolbar, root, false);
root.addView(toolbar, 0);
} else {
toolbar = null;
}
setSupportActionBar(toolbar);
getSupportActionBar().setDisplayHomeAsUpEnabled(true);
}
}
My toolbar is
toolbar.xml
<android.support.v7.widget.Toolbar xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:id="#+id/toolbar"
android:layout_height="wrap_content"
android:layout_width="match_parent"
android:minHeight="?attr/actionBarSize"
android:background="?attr/colorPrimary"
android:elevation="4dp"
app:theme="#style/ThemeOverlay.AppCompat.Dark.ActionBar"
app:popupTheme="#style/ThemeOverlay.AppCompat.Light" >
<!-- android:elevation="4dp" is used due to http://www.google.com/design/spec/what-is-material/elevation-shadows.html#elevation-shadows-elevation-android- -->
</android.support.v7.widget.Toolbar>
I was wondering, what I had did wrong, which causes Toolbar overlapped with PreferenceFragment

After some research and getting advice from Ian Lake, I manage to make it work by
Change from AppCompatPreferenceActivity, to AppCompatActivity+PreferenceFragmentCompat
The following solution is workable for API 10 and above.
JStockPreferenceActivity.java
public class JStockPreferenceActivity extends AppCompatActivity {
#SuppressWarnings("deprecation")
#SuppressLint("NewApi")
#Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.jstock_preference_activity);
initToolbar();
if (savedInstanceState == null) {
// Display the fragment as the main content.
JStockPreferenceFragment jStockPreferenceFragment = JStockPreferenceFragment.newInstance();
jStockPreferenceFragment.setArguments(this.getIntent().getExtras());
this.getSupportFragmentManager().beginTransaction().replace(R.id.content, jStockPreferenceFragment).commit();
}
}
JStockPreferenceFragment.java
public class JStockPreferenceFragment extends PreferenceFragmentCompat implements SharedPreferences.OnSharedPreferenceChangeListener, PreferenceFragmentCompat.OnPreferenceDisplayDialogCallback {
public static JStockPreferenceFragment newInstance() {
return new JStockPreferenceFragment();
}
However, by doing so, there is a shortcoming. Your previous custom DialogPreference no longer work. To solve this, you need to
Replace DialogPreference with PreferenceDialogFragmentCompat.
Replace DialogPreference in XML with ListPreference. (I'm not exactly sure whether ListPreference as replacement is a right way. It works anyway)
Manually show PreferenceDialogFragmentCompat, in PreferenceFragmentCompat's onPreferenceDisplayDialog.
JStockPreferenceFragment.java
public class JStockPreferenceFragment extends PreferenceFragmentCompat implements SharedPreferences.OnSharedPreferenceChangeListener, PreferenceFragmentCompat.OnPreferenceDisplayDialogCallback {
public static JStockPreferenceFragment newInstance() {
return new JStockPreferenceFragment();
}
////////////////////////////////////////////////////////////////////////////////////////////////
// Showing custom dialog preference.
private void showPreferenceDialogFragmentCompat(PreferenceDialogFragmentCompat preferenceDialogFragmentCompat) {
preferenceDialogFragmentCompat.setTargetFragment(this, 0);
preferenceDialogFragmentCompat.show(this.getFragmentManager(), "android.support.v7.preference.PreferenceFragment.DIALOG");
}
private void showCurrencySymbolPreferenceDialogFragmentCompat(Preference preference) {
CurrencySymbolPreferenceDialogFragmentCompat currencySymbolPreferenceDialogFragmentCompat = CurrencySymbolPreferenceDialogFragmentCompat.newInstance(preference.getKey());
showPreferenceDialogFragmentCompat(currencySymbolPreferenceDialogFragmentCompat);
}
// Showing custom dialog preference.
////////////////////////////////////////////////////////////////////////////////////////////////
////////////////////////////////////////////////////////////////////////////////////////////////
// Callback when we are using custom dialog preference.
#Override
public Fragment getCallbackFragment() {
return this;
}
#Override
public boolean onPreferenceDisplayDialog(PreferenceFragmentCompat preferenceFragmentCompat, Preference preference) {
final String key = preference.getKey();
if (_CURRENCY_SYMBOL_OPTIONS.equals(key)) {
showCurrencySymbolPreferenceDialogFragmentCompat(preference);
return true;
}
return false;
}
// Callback when we are using custom dialog preference.
////////////////////////////////////////////////////////////////////////////////////////////////
CurrencySymbolPreferenceDialogFragmentCompat.java
public class CurrencySymbolPreferenceDialogFragmentCompat extends PreferenceDialogFragmentCompat {
public CurrencySymbolPreferenceDialogFragmentCompat() {
}
public static CurrencySymbolPreferenceDialogFragmentCompat newInstance(String key) {
CurrencySymbolPreferenceDialogFragmentCompat fragment = new CurrencySymbolPreferenceDialogFragmentCompat();
Bundle b = new Bundle(1);
b.putString("key", key);
fragment.setArguments(b);
return fragment;
}
#Override
public void onDialogClosed(boolean positiveResult) {
if (positiveResult) {
...
this.getPreference().setSummary(getSummary());
...
}
}
}
preferences.xml
<android.support.v7.preference.ListPreference
android:title="#string/preference_currency_symbol_title"
android:key="_CURRENCY_SYMBOL_OPTIONS" />
Please note that, you need to add the following item in your theme.
<item name="preferenceTheme">#style/PreferenceThemeOverlay</item>
Some bugs regarding theme
However, the theme-ing isn't perfect yet - PreferenceFragmentCompat requires preferenceTheme to be set
This is a known issues : https://code.google.com/p/android/issues/detail?id=183376
There is proposed workaround https://stackoverflow.com/a/32108439/72437 But, the workaround will only work for v14 and above, not v7 and above.

Related

How to handle navigation drawer's event in another activity?

I have a main activity with a lot of fragments. I have implemented the navigation drawer inside the main activity so that when I am viewing the fragments, I can open the navigation drawer as well. Inside the main_activity.xml layout file (refer below), I have used <include layout="#layout/navigation_drawer"/> to include the layout of the navigation drawer. Inside the navigation drawer, I have a lot of buttons and texts that redirect the user to different fragments.
Currently, I am handling all the onClick events of the navigation drawer inside the main activity. However, this makes my code inside the main activity very long and very hard to manage (in terms of readability and whether it is easy to edit). How to handle the onClick events in another class that specializes in handling the navigation drawer's event? What is the best way to achieve this?
main_activity.xml
<android.support.v4.widget.DrawerLayout
xmlns:android="http://schemas.android.com/apk/res/android"
android:id="#+id/drawer"
android:layout_height="match_parent"
android:layout_width="match_parent">
<!-- The main content view -->
<FrameLayout
android:id="#+id/fragmentContainer"
android:layout_width="match_parent"
android:layout_height="match_parent" />
<!-- The navigation drawer -->
<LinearLayout
android:id="#+id/drawerContent"
android:layout_width="250dp"
android:layout_height="match_parent"
android:layout_gravity="start"
android:orientation="vertical">
<include layout="#layout/navigation_drawer"/>
</Linear Layout>
</android.support.v4.widget.DrawerLayout>
MainActivity class
public class MainActivity extends FragmentActivity implements View.OnClickListener, DrawerLayout.DrawerListener {
Button button1;
TextView text1;
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.main_activity);
button1 = (Button)findViewById(R.id.button1);
text1 = (TextView)findViewById(R.id.text1);
button1.setOnClickListener(this);
text1.setOnClickListener(this);
}
public void onClick(View v){
//I handle all onClick events of navigation drawer here.
//I want to move all of these to another class.
//If possible, I would like to move the onClick method and declaration of Button and TextView variables too.
}
}
Create a class which implement View.OnClickListener interface and pass newly created class object to widget setonclicklistner method
public class MainActivity extends FragmentActivity implements View.OnClickListener, DrawerLayout.DrawerListener {
Button button1;
TextView text1;
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.main_activity);
button1 = (Button)findViewById(R.id.button1);
text1 = (TextView)findViewById(R.id.text1);
button1.setOnClickListener(new ClickHander());
}}
public class ClickHander implements View.OnClickListener
{
#Override
public void onClick(View v) {
}
}
I suggest another operation: MVP architecture.
By using this kind of architecture you separate views from logics using a presenter.
The goal is to have more classes which contain small pieces of code so that's all unit testable and maintenable too.
Act this way:
Create a Presenter Interface which contains a init() method and all the methods you need, passing all arguments (but not the views) inside the methods themselves
Create a PresenterImplementation class which implements this interface
Create an ActivityView Interface
Implement this interface in your Activity so that when PresenterImplementation logic has been executed, in the same PresenterImplementation class you can call the view.
Here's a basic example, you can modify it to fit your needs.
MyActivityPresenter.java
public interface MyActivityPresenter {
void init();
/**
* This method is for example only
* #param intent You can pass whatever you want as argument, but don't pass views
* because It would be formally uncorrect
*/
void onTextViewClicked(Intent intent);
}
MyActivityPresenterImpl.java
public class MyActivityPresenterImpl implements MyActivityPresenter {
private Activity activity;
private MyActivityPresenterView view;
public MyActivityPresenterImpl(Activity activity, MyActivityPresenterView view) {
this.activity = activity;
this.view = view;
}
#Override
public void init() {
//if(some condition or nothing at all){
view.initViews();
//}
}
#Override
public void onTextViewClicked(Intent intent) {
//Do controls in Presenter Implementation
if(intent != null){
Fragment fragment = new SomeFragment();
Bundle bundle = intent.getExtras();
if(bundle != null){
fragment.setArguments(bundle);
}
view.loadFragment(fragment);
}
}
}
MyAppBaseView.java
public interface MyAppBaseView {
//Just created in order to be extended from every interface and to not rewrite everytime from scratch each method
void loadFragment(Fragment fragment);
}
MyActivityView.java
public interface MyActivityView extends MyAppBaseView {
void initViews();
}
MyActivity.java
public class MyActivity extends AppCompatActivity implements View.OnClickListener,MyActivityView {
private TextView textView;
private Intent intent;
private MyActivityPresenter presenter;
#Override
protected void onCreate(#Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.something);
presenter = new MyActivityPresenterImpl(this,this);
presenter.init();
this.intent = getIntent();
}
#Override
public void initViews() {
textView = findViewById(R.id.text_view_title);
textView.setOnClickListener(this);
}
#Override
public void loadFragment(Fragment fragment) {
//Transaction
}
#Override
public void onClick(View v) {
presenter.onTextViewClicked(intent);
}
}
This is only for example.
Forma mentis should be to separate logic from views. So that all gets more readable, testable and maintainable.
Hope it helps.

Inner PreferenceScreen does not open with PreferenceFragmentCompat

My inner PreferenceScreen of PreferenceFragmentCompat is not showing, or seems to ignore tapping events.
I created MyPreferenceFragment that extends PreferenceFragmentCompat
public class MyPreferenceFragment extends PreferenceFragmentCompat {
#Override
public void onCreatePreferences(Bundle savedInstanceState, String rootKey) {
addPreferencesFromResource(R.xml.preferences);
}
}
then I changed my theme at styles.xml like
<style name="AppTheme" parent="#style/Theme.AppCompat.Light">
<item name="preferenceTheme">#style/PreferenceThemeOverlay</item>
</style>
And finally create my preferences.xml file like
<PreferenceScreen xmlns:android="http://schemas.android.com/apk/res/android">
<CheckBoxPreference android:title="Check Me"/>
<PreferenceScreen android:title="My Screen"> <!-- This is not opening -->
<EditTextPreference android:title="Edit text" />
</PreferenceScreen>
</PreferenceScreen>
At the build.gradle I have added both:
compile 'com.android.support:appcompat-v7:23.0.1'
compile 'com.android.support:preference-v7:23.0.1'
code of the Activity
public class MainActivity extends AppCompatActivity {
#Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
}
}
activity_main.xml
<fragment xmlns:android="http://schemas.android.com/apk/res/android"
android:id="#+id/fragment"
android:name="com.mando.preferenceapp.MyPreferenceFragment"
android:layout_width="match_parent"
android:layout_height="match_parent" />
Testing the above code I cannot open / get into the preference screen. Am I missing something? Why this isn't working?
After spending many many hours with tries, searching and thankfully with some assistance from the creators of the support library. I've managed to make it work.
Step 1. Activity
public class MyActivity extends AppCompatActivity implements
PreferenceFragmentCompat.OnPreferenceStartScreenCallback {
#Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
if (savedInstanceState == null) {
// Create the fragment only when the activity is created for the first time.
// ie. not after orientation changes
Fragment fragment = getSupportFragmentManager().findFragmentByTag(MyPreferenceFragment.FRAGMENT_TAG);
if (fragment == null) {
fragment = new MyPreferenceFragment();
}
FragmentTransaction ft = getSupportFragmentManager().beginTransaction();
ft.replace(R.id.fragment_container, fragment, MyPreferenceFragment.FRAGMENT_TAG);
ft.commit();
}
}
#Override
public boolean onPreferenceStartScreen(PreferenceFragmentCompat preferenceFragmentCompat,
PreferenceScreen preferenceScreen) {
FragmentTransaction ft = getSupportFragmentManager().beginTransaction();
MyPreferenceFragment fragment = new MyPreferenceFragment();
Bundle args = new Bundle();
args.putString(PreferenceFragmentCompat.ARG_PREFERENCE_ROOT, preferenceScreen.getKey());
fragment.setArguments(args);
ft.replace(R.id.fragment_container, fragment, preferenceScreen.getKey());
ft.addToBackStack(preferenceScreen.getKey());
ft.commit();
return true;
}
}
Tips.
Do not add the fragment by xml you will have crashes on orientation changes.
Handle the recreations of activity / fragment add in onCreate so as to avoid losing your fragment when inside a preference screen.
The host activity of the fragment should implement the PreferenceFragmentCompat.OnPreferenceStartScreenCallback and recreate fragments of the same instance.
Step 2. PreferenceFragment
public class MyPreferenceFragment extends PreferenceFragmentCompat {
public static final String FRAGMENT_TAG = "my_preference_fragment";
public MyPreferenceFragment() {
}
#Override
public void onCreatePreferences(Bundle bundle, String rootKey) {
setPreferencesFromResource(R.xml.preferences, rootKey);
}
}
Tips.
Use the method setPreferencesFromResource and take advantage of the rootKey of each screen. This way your code will be reused properly.
Keep in mind that if you have code like findPreference in your fragment it should have null checks as when you were in inner screens this will give you nothing.
The thing that is missing now is the implementation of the back arrow in the actionbar (home action) but this never works by itself ;-)
I' also created a demo app wrapping all this code you can find it on github.
Solution is to start another fragment of the same class but with different root key. No Activity actions involved.
#Override
public void onCreatePreferences(Bundle savedInstanceState, String rootKey){
if(getArguments() != null){
String key = getArguments().getString("rootKey");
setPreferencesFromResource(R.xml.preferences, key);
}else{
setPreferencesFromResource(R.xml.preferences, rootKey);
}
}
#Override
public void onNavigateToScreen(PreferenceScreen preferenceScreen){
ApplicationPreferencesFragment applicationPreferencesFragment = new ApplicationPreferencesFragment();
Bundle args = new Bundle();
args.putString("rootKey", preferenceScreen.getKey());
applicationPreferencesFragment.setArguments(args);
getFragmentManager()
.beginTransaction()
.replace(getId(), applicationPreferencesFragment)
.addToBackStack(null)
.commit();
}
I did it slightly differently, I'm launching a new activity for each screen. This seems to require less hacks: no need to mess with swapping fragments and background colors. You also get activity change animation as a bonus!
public class PreferencesActivity extends AppCompatActivity implements PreferenceFragmentCompat.OnPreferenceStartScreenCallback {
final static private String KEY = "key";
#Override protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.preferences);
setSupportActionBar((Toolbar) findViewById(R.id.toolbar));
ActionBar actionBar = getSupportActionBar();
if (actionBar != null) actionBar.setDisplayHomeAsUpEnabled(true);
if (savedInstanceState != null)
return;
Fragment p = new PreferencesFragment();
String key = getIntent().getStringExtra(KEY);
if (key != null) {
Bundle args = new Bundle();
args.putString(PreferenceFragmentCompat.ARG_PREFERENCE_ROOT, key);
p.setArguments(args);
}
getSupportFragmentManager().beginTransaction()
.add(R.id.preferences, p, null)
.commit();
}
#Override public boolean onPreferenceStartScreen(PreferenceFragmentCompat preferenceFragmentCompat, PreferenceScreen preferenceScreen) {
Intent intent = new Intent(PreferencesActivity.this, PreferencesActivity.class);
intent.putExtra(KEY, preferenceScreen.getKey());
startActivity(intent);
return true;
}
#Override public boolean onOptionsItemSelected(MenuItem item) {
if (item.getItemId() == android.R.id.home) {
onBackPressed();
return true;
}
return super.onOptionsItemSelected(item);
}
public static class PreferencesFragment extends PreferenceFragmentCompat implements ... {
private static final String FRAGMENT_DIALOG_TAG = "android.support.v7.preference.PreferenceFragment.DIALOG";
private String key;
#Override public void onCreatePreferences(Bundle bundle, String key) {
setPreferencesFromResource(R.xml.preferences, this.key = key);
}
// this only sets the title of the action bar
#Override public void onActivityCreated(Bundle savedInstanceState) {
ActionBar actionBar = ((AppCompatActivity) getActivity()).getSupportActionBar();
if (actionBar != null) actionBar.setTitle((key == null) ? "Settings" : findPreference(key).getTitle());
super.onActivityCreated(savedInstanceState);
}
}
}
xml:
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_margin="0dp"
android:orientation="vertical"
android:padding="0dp"
android:id="#+id/preferences">
<android.support.v7.widget.Toolbar
android:id="#+id/toolbar"
android:layout_width="match_parent"
android:layout_height="?attr/actionBarSize"
android:background="?attr/colorPrimary" />
<!-- preference fragment will be inserted here programmatically -->
</LinearLayout>
Another solution is to track the preference screens yourself and use the PreferenceFragmentCompat api
Here's the basic solution. (It doesn't cover all the edge cases, see advanced solution below)
Ensure you have configChanges="orientation" to prevent create/destroy
<activity
android:name=".MyPreferencesActivity"
android:configChanges="orientation" />
In the Activity you want to keep a Stack of PreferenceScreens and push/pop as needed
/* track the screens as a Stack */
private Stack<PreferenceScreen> preferenceScreens = new Stack<>();
// ensure your Activity implements PreferenceFragmentCompat.OnPreferenceStartScreenCallback
#Override
public boolean onPreferenceStartScreen(PreferenceFragmentCompat preferenceFragmentCompat, PreferenceScreen preferenceScreen) {
preferenceScreens.push(preferenceFragmentCompat.getPreferenceScreen());
preferenceFragmentCompat.setPreferenceScreen(preferenceScreen);
return true;
}
#Override
public void onBackPressed() {
if (preferenceScreens.empty()) {
super.onBackPressed();
} else {
prefsFragment.setPreferenceScreen(preferenceScreens.pop());
}
}
Optional: In your Fragment that extends PreferenceFragmentCompat, add setRetainInstance(true). (Note that without
this it will likely work also, but it 'could' break occasionally. If you set 'Don't keep Activities' to true, and
you'll see that it will get collected)
#Override
public void onCreatePreferences(Bundle savedInstanceState, String rootKey) {
setRetainInstance(true);
// Load the preferences from an XML resource
setPreferencesFromResource(R.xml.preferences, rootKey);
...
That's it! Except that if you want to cover edge cases...
Advanced Solution (If you set 'Don't Keep Activities to True, you'll need to ensure you can rebuild everything from savedInstanceState)
Note that the accepted answer doesn't actually preserve state.
set 'Don't Keep Activities' to True
navigate to a nested PreferenceScreen
Press home and then navigate back to the app
It 'should' still be on the Nested PreferenceScreen, but it's actually on the root one
Full Advanced Solution using PreferenceFragmentCompat api and preserving the PreferenceScreen stack
import android.os.Bundle;
import android.support.v7.app.AppCompatActivity;
import android.support.v7.preference.PreferenceFragmentCompat;
import android.support.v7.preference.PreferenceScreen;
import java.util.ArrayList;
import java.util.Objects;
import java.util.Stack;
/**
* Class to Show the preference screen with Activity keeping state
* #author Aaron Vargas
*/
public class MyPreferencesActivityStateful extends AppCompatActivity implements PreferenceFragmentCompat.OnPreferenceStartScreenCallback {
private static final String PREFERENCE_SCREENS = "PREFERENCE_SCREENS";
private PrefsFragment prefsFragment;
private Stack<PreferenceScreen> preferenceScreens = new Stack<>();
#Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
// Display the fragment as the main content. Re-Use if possible
String tag = PrefsFragment.class.getName();
prefsFragment = (PrefsFragment) getSupportFragmentManager().findFragmentByTag(tag);
if (prefsFragment == null) prefsFragment = new PrefsFragment();
getSupportFragmentManager().beginTransaction().replace(android.R.id.content,
prefsFragment, tag).commit();
}
#Override
protected void onRestoreInstanceState(Bundle savedInstanceState) {
super.onRestoreInstanceState(savedInstanceState);
// rebuild preferenceScreen stack
for (String screenKey : Objects.requireNonNull(savedInstanceState.getStringArrayList(PREFERENCE_SCREENS))) {
preferenceScreens.push((PreferenceScreen) prefsFragment.findPreference(screenKey));
}
PreferenceScreen preferenceScreen = preferenceScreens.pop();
if (preferenceScreen != prefsFragment.getPreferenceScreen()) { // optimize if same
prefsFragment.setPreferenceScreen(preferenceScreen);
}
}
#Override
public boolean onPreferenceStartScreen(PreferenceFragmentCompat preferenceFragmentCompat, PreferenceScreen preferenceScreen) {
preferenceScreens.push(preferenceFragmentCompat.getPreferenceScreen());
preferenceFragmentCompat.setPreferenceScreen(preferenceScreen);
return true;
}
#Override
public void onBackPressed() {
// account for onRestore not getting called equally to onSave
while (preferenceScreens.contains(prefsFragment.getPreferenceScreen())) {
preferenceScreens.remove(prefsFragment.getPreferenceScreen());
}
if (preferenceScreens.empty()) {
super.onBackPressed();
} else {
prefsFragment.setPreferenceScreen(preferenceScreens.pop());
}
}
#Override
protected void onSaveInstanceState(Bundle outState) {
super.onSaveInstanceState(outState);
preferenceScreens.push(prefsFragment.getPreferenceScreen());
ArrayList<String> keys = new ArrayList<>(preferenceScreens.size());
for (PreferenceScreen screen : preferenceScreens) {
keys.add(screen.getKey());
}
outState.putStringArrayList(PREFERENCE_SCREENS, keys);
}
public static class PrefsFragment extends PreferenceFragmentCompat {
#Override
public void onCreatePreferences(Bundle savedInstanceState, String rootKey) {
setRetainInstance(true); // ensure in manifest - android:configChanges="orientation"
// Load the preferences from an XML resource
setPreferencesFromResource(R.xml.preferences, rootKey);
}
}
}
You can also handle all this in your Fragment instead of the Activity. Here's a gist of that https://gist.github.com/aaronvargas/0f210ad8643b512efda4acfd524e1232
Using Navigation Component (Android Jetpack) and Kotlin it's very easy now:
class PrefsFragment : PreferenceFragmentCompat() {
private val args: PrefsFragmentArgs by navArgs()
override fun onCreatePreferences(state: Bundle?, rootKey: String?) {
setPreferencesFromResource(R.xml.prefs, args.rootKey)
}
override fun onNavigateToScreen(preferenceScreen: PreferenceScreen?) {
findNavController().navigate(
PrefsFragmentDirections.changeRoot(preferenceScreen!!.key)
)
}
}
Based on #squirrel Intent solution, I made it work this way. It requires even less hacking.
Activity:
import android.support.v7.app.AppCompatActivity;
public class SettingsActivity extends AppCompatActivity {
public static final String TARGET_SETTING_PAGE = "target";
#Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
SettingsFragment settingsFragment = new SettingsFragment();
Intent intent = getIntent();
if (intent != null) {
String rootKey = intent.getStringExtra(TARGET_SETTING_PAGE);
if (rootKey != null) {
settingsFragment.setArguments(Bundler.single(TARGET_SETTING_PAGE, rootKey));
}
}
getFragmentManager().beginTransaction()
.replace(android.R.id.content, settingsFragment)
.commit();
}
}
Fragment:
import android.support.v14.preference.PreferenceFragment;
public class SettingsFragment extends PreferenceFragment {
#Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
Bundle arguments = getArguments();
if (arguments != null && arguments.getString(TARGET_SETTING_PAGE) != null) {
setPreferencesFromResource(R.xml.preferences, arguments.getString(TARGET_SETTING_PAGE));
} else {
addPreferencesFromResource(R.xml.preferences);
}
}
#Override
public void onNavigateToScreen(PreferenceScreen preferenceScreen) {
Intent intent = new Intent(getActivity(), SettingsActivity.class)
.putExtra(TARGET_SETTING_PAGE, preferenceScreen.getKey());
startActivity(intent);
super.onNavigateToScreen(preferenceScreen);
}
}
It is sad you need so much hacks in the support appcompat libraries for something that works flawlessly out-of-the-box in standard android.
Alternative using Navigation component + androidx.appcomat:
https://stackoverflow.com/a/59732509/5437789
With this, you wont loose the back stack and go back to main page settings when you press back button.
Here is a simple solution from android documentation. To implement inner preference screen navigation with PreferenceFragmentCompact all you have to do is add fragment attribute to the embedded preference screen giving the fragment full path to navigate to eg. com.example.FragmentName.
Sample code:
<PreferenceCategory app:title="#string/choose_theme"
android:icon="#drawable/ic_baseline_color_lens_24">
<SwitchPreference
android:title="#string/apply_night_mode"
android:key="#string/key_enable_night_mode"/>
<PreferenceScreen
android:fragment="com.example.simbokeyboard.BlankFragment"
android:title="Custom Theme"
android:summary="#string/theme_summary">
<Preference
android:key="#string/choose_theme"
android:title="#string/choose_theme"
android:layout="#layout/theme_chooser"/>
</PreferenceScreen>
</PreferenceCategory>

Calling Toolbar on each Activity

My app has a toolbar that should be present on every view. Currently, I do the following in my onCreate() method for each Activity I have:
Toolbar toolbar = (Toolbar) findViewById(R.id.toolbar);
setSupportActionBar(toolbar);
Does this need to be done in every onCreate() method in every Activity or is there a simpler way? Also, as a side question, how can I implement a "back" feature in the toolbar that takes the user back one action if they click it?
Create a Base class for Activity
public abstract class BaseActivity extends AppCompatActivity {
Toolbar toolbar;
#Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(getLayoutResource());
configureToolbar();
}
protected abstract int getLayoutResource();
private void configureToolbar() {
toolbar = (Toolbar) findViewById(R.id.toolbar);
if (toolbar != null) {
setSupportActionBar(toolbar);
getSupportActionBar().setDisplayHomeAsUpEnabled(true);
}
}
#Override
public boolean onOptionsItemSelected(MenuItem item) {
switch (item.getItemId()) {
case android.R.id.home:
FragmentManager fm = getSupportFragmentManager();
if (fm != null && fm.getBackStackEntryCount() > 0) {
fm.popBackStack(null, FragmentManager.POP_BACK_STACK_INCLUSIVE);
} else {
finish();
}
return true;
default:
return super.onOptionsItemSelected(item);
}
}
}
And in each Activity extends this BaseActivity to get the ToolBar and implementing the back feature.
At last don't forget to include the ToolBar in each activity layout.
Edit:
Override that method getLayoutResource() in each Activity and pass the layout id.
public class MainActivity extends BaseActivity {
#Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
}
#Override
public int getLayoutResource() {
return R.layout.activity_main;
}
This is my implementation. It removes the need of the getLayoutResources() from the accepted answer and brings back the "setContentView()" in all activities as normal
public abstract class BaseActivity extends AppCompatActivity {
Toolbar toolbar;
#Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
}
protected boolean useToolbar() {
return true;
}
#Override
public void setContentView(int layoutResID) {
View view = getLayoutInflater().inflate(layoutResID, null);
configureToolbar(view);
super.setContentView(view);
}
private void configureToolbar(View view) {
toolbar = (Toolbar) view.findViewById(R.id.toolbar);
if (toolbar != null) {
if (useToolbar()) {
setSupportActionBar(toolbar);
getSupportActionBar().setDisplayHomeAsUpEnabled(true);
} else {
toolbar.setVisibility(View.GONE);
}
}
}
}
From here on you just extend BaseActivity. If you don't want a toolbar you will have to override the useToolbar().
Don't forget to add in activity.xml at the top
<include layout="#layout/toolbar" />
toolbar.xml
<?xml version="1.0" encoding="utf-8"?>
<merge xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="wrap_content">
<android.support.v7.widget.Toolbar
android:id="#+id/toolbar"
android:layout_width="match_parent"
android:layout_height="?attr/actionBarSize"
android:background="?attr/colorPrimary"
app:layout_scrollFlags="scroll|enterAlways"
app:popupTheme="#style/ThemeOverlay.AppCompat.Light" />
</merge>
It depends on your implementation but if you want avoid boilerplate code you should use good programming OO.
An Example using Fragment.
public abstract class FragmentBase extends Fragment {
protected void settingsToolbar(View rootView) {
Toolbar toolbar = (Toolbar) rootView.findViewById(R.id.toolbar);
((AppCompatActivity) getActivity()).setSupportActionBar(toolbar);
final ActionBar actionBar = ((AppCompatActivity) getActivity()).getSupportActionBar();
if (actionBar != null) {
// TODO add your code and your requirements
actionBar.setDisplayShowTitleEnabled(true);
actionBar.setDisplayHomeAsUpEnabled(true);
}
}
}
I hope this can give you an idea.
If you have used Activity then Create BaseActivity that extends AppCompatActivity or ActionBarActivity(Deprecated) and move Toolbar code to BaseActivity.
public class BaseActivity extends AppCompatActivity {
#Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.main);
Toolbar toolbar = (Toolbar) findViewById(R.id.toolbar);
setSupportActionBar(toolbar);
}
}
If you have used Fragment then Create BaseFragment that extends Fragment and move Toolbar code to BaseFragment.
public class BaseFragment extends Fragment {
View main;
#Override
public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
main = inflater.inflate(R.layout.fragment_about, container, false);
Toolbar toolbar = (Toolbar) main.findViewById(R.id.toolbar);
getActivity().setSupportActionBar(toolbar);
return main;
}
}
In main XML layout you have to add Toolbar xml code.
Now in every view(Activity) extends BaseActivity instead of AppCompatActivity or ActionBarActivity so you can get access Toolbar in every view.
public class YourActivity extends BaseActivity{
//your code
}
EDIT1:
main.xml
<RelativeLayout
xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent">
<android.support.v7.widget.Toolbar
android:id="#+id/toolbar"
android:theme="#style/toolbarTheme"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="#color/green"
android:minHeight="?attr/actionBarSize" />
</RelativeLayout>
EDIT2:
getSupportActionBar().setDisplayHomeAsUpEnabled(true);
getSupportActionBar().setHomeButtonEnabled(true);
add these two lines below setSupportActionBar(toolbar); in BaseActivity.
I hope it helps!
Create a base activity and initialize your tool bar in this class. Now it can be extends to all other child activity.
FirtActivity extends BaseActivity
SecondActivity extends BaseActivity
In base activity toll bar back button click you can check like below mentioned way
if(this instance of FirstActivity){
//do stuff here
}else if(this instance of SecondActivity){
//do stuff here
}

How to maintain ListView fragment state after orientation/configuration change?

I have a ListView fragment in my application that is experiencing some issues.
When I scroll to a specific element in the list view, then rotate the device, the list view resets to the top of the list.
When I enter multi-select mode in the list view, then rotate the device, the selected list items reset.
Here is my activity:
public class TestActivity extends ActionBarActivity
{
protected static final String FRAGMENT_TAG = "TEST";
#Override
protected void onCreate(Bundle savedInstanceState)
{
super.onCreate(savedInstanceState);
setContentView(R.layout.main_layout);
Toolbar toolbar = (Toolbar)findViewById(R.id.toolbar);
setSupportActionBar(toolbar);
FragmentManager fm = getFragmentManager();
TestFragment f = (TestFragment)fm.findFragmentByTag(FRAGMENT_TAG);
// If no fragment exists, then create a new one and add it!
if (f == null)
{
fm.beginTransaction().add(R.id.fragment_holder, new TestFragment(), FRAGMENT_TAG)
.commit();
}
}
}
here is main_layout.xml:
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical" >
<android.support.v7.widget.Toolbar
android:id="#+id/toolbar"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="?attr/colorPrimary"
android:minHeight="?attr/actionBarSize" >
</android.support.v7.widget.Toolbar>
<FrameLayout
android:id="#+id/fragment_holder"
android:layout_width="match_parent"
android:layout_height="match_parent" />
</LinearLayout>
Here is my TestFragment class, with the misc. content removed:
public class TestFragment extends ListFragment
{
#Override
public void onActivityCreated(Bundle savedInstanceState)
{
super.onActivityCreated(savedInstanceState);
}
#Override
public void onViewCreated(View view, Bundle savedInstanceState)
{
super.onViewCreated(view, savedInstanceState);
// Sets the list up for multiple choice selection.
ListView listView = getListView();
listView.setChoiceMode(ListView.CHOICE_MODE_MULTIPLE_MODAL);
listView.setMultiChoiceModeListener(this);
}
}
I have seen two discussions for this. One is stating that I should be using setRetainInstance(true) in the fragment's onCreate(Bundle savedInstanceState) method. The other says I should be using the onSaveInstanceState(Bundle bundle) and onRestoreInstanceState(Bundle bundle) methods to keep track of stuff somehow. I would like to use the setRetainInstanceState(true) approach, but adding that into the project like so:
#Override
public void onCreate(Bundle savedInstanceState)
{
super.onCreate(savedInstanceState);
setRetainInstanceState(true);
}
Does not work for me. What am I doing wrong here?
I would suggest a slightly different approach to adding a Fragment to your Activity.
In your TestActivity class, check if the Bundle is null in the onCreate() method. Only add the Fragment if savedInstanceState is null. This will prevent your Activity from adding another instance of the same Fragment when the device's orientation is changed.
public class TestActivity extends ActionBarActivity {
protected static final String FRAGMENT_TAG = "TEST";
#Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.main_layout);
Toolbar toolbar = (Toolbar)findViewById(R.id.toolbar);
setSupportActionBar(toolbar);
if (savedInstanceState == null) {
getFragmentManager().beginTransaction()
.replace(R.id.fragment_holder, new TestFragment(), FRAGMENT_TAG)
.commit();
}
}
}
I would suggest not using the setRetainInstance(true) method for a Fragment that has a UI. Check the StackOverflow discussion here for an explanation of when to use setRetainInstance(true).

Is it possible to auto-scroll down a PreferenceFragment to a desired (settings) entry?

I'm using startActivity(new Intent(this, SettingsActivity.class)) from an Activity to call the PreferenceFragment of my app when a certain user interaction is required to be performed into the preferences.
I think would be good to scroll down to reach the involved preference.
I found solutions about how to scroll views into ScrollViews and ListViews, but nothing related to PreferenceFragments.
My preferences look like:
public class SettingsActivity extends Activity {
#Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
// ...
PreferenceManager.setDefaultValues(this, R.xml.settings, false);
getFragmentManager().beginTransaction()
.replace(android.R.id.content, new SettingsFragment())
.commit();
}
public static class SettingsFragment extends PreferenceFragment implements OnSharedPreferenceChangeListener {
//preferences:
private Preference p
// [...]
#Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
addPreferencesFromResource(R.xml.settings);
//preference click listeners...
p = (Preference) findPreference("PR_NAME");
p.setOnPreferenceClickListener(new OnPreferenceClickListener() {
public boolean onPreferenceClick(Preference preference) {
// ...
return true;
}
});
// [...]
}
}
}
Thanks for any suggestion
To scroll in PreferenceScreen (Setting/Preferences), Use this for Android API 24.1.+ or with androidX API
public void scrollToPreference (String key)
yes, it is possible but it is hacky so nor recommended. it assumes that Preferences are backed by a ListView with id == android.R.id.list, so when you have reference to that ListView you can call setSelection(position)

Categories

Resources