Appcompatactivity with custom native (not compatib

2019-08-01 14:24发布

问题:

I am building a preferences / settings screen inside an Android AppCompatActivity. One requirement is a custom [DialogPreference][1] with a TimePicker.

The DialogPreference must be 'native', meaning not the compatibility version like described here and here.

The code for the AppCompatActivity:

...

public class SettingsActivity extends AppCompatActivity
{
    @Override
    protected void onCreate(Bundle savedInstanceState)
    {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_preferences);

        Toolbar toolbar = (Toolbar) findViewById(R.id.toolbar_settings);
        setSupportActionBar(toolbar);
        getSupportActionBar().setDisplayHomeAsUpEnabled(true);
    }
}

The layout of activity_preferences.xml:

...

    <android.support.v4.widget.NestedScrollView
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:scrollbars="vertical"
        app:layout_behavior="@string/appbar_scrolling_view_behavior">

        <fragment
            android:name="nl.waywayway.broodkruimels.SettingsFragment"
            android:id="@+id/settings_fragment"
            android:layout_width="match_parent"
            android:layout_height="match_parent" />

    </android.support.v4.widget.NestedScrollView>

The SettingsFragment class:

...

public class SettingsFragment extends PreferenceFragment
{
    Context mContext;

    @Override
    public void onCreate(Bundle savedInstanceState)
    {
        super.onCreate(savedInstanceState);
        addPreferencesFromResource(R.xml.preferences);
    }
}

The preferences.xml file:

<?xml version="1.0" encoding="utf-8"?>
<PreferenceScreen
    xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent">

    <SwitchPreference
        android:key="pref_notify"
        android:title="@string/pref_notify"
        android:summary="@string/pref_notify_summ"
        android:defaultValue="false" />

    <nl.waywayway.broodkruimels.TimePreference
        android:dependency="pref_notify"
        android:key="pref_notify_time"
        android:title="@string/pref_notify_time"
        android:summary="@string/pref_notify_time_summ"
        android:defaultValue="390" />

</PreferenceScreen>

And the custom TimePreference class:

public class TimePreference extends DialogPreference
{
    private TimePicker mTimePicker = null;
    private int mTime;
    private int mDialogLayoutResId = R.layout.preferences_timepicker_dialog;

    // 4 constructors for the API levels,
    // calling each other

    public TimePreference(Context context)
    {
        this(context, null);
    }

    public TimePreference(Context context, AttributeSet attrs)
    {
        this(context, attrs, R.attr.preferenceStyle);
    }

    public  TimePreference(Context context, AttributeSet attrs, int defStyleAttr)
    {
        this(context, attrs, defStyleAttr, defStyleAttr);
    }

    public  TimePreference(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes)
    {
        super(context, attrs, defStyleAttr, defStyleRes);
    }

    public int getTime()
    {
        return  mTime;
    }

    public void setTime(int time)
    {
        mTime = time;

        // Save to Shared Preferences
        persistInt(time);
    }

    @Override
    public int getDialogLayoutResource()
    {
        return mDialogLayoutResId;
    }

    @Override
    protected Object onGetDefaultValue(TypedArray a, int index)
    {
        //  Default  value  from  attribute.  Fallback  value  is  set  to  0.
        return a.getInt(index,  0);
    }

    @Override
    protected void onSetInitialValue(boolean restorePersistedValue, Object defaultValue)
    {
        // Read the value. Use the default value if it is not possible.
        setTime(restorePersistedValue ?
                getPersistedInt(mTime) : (int) defaultValue);
    }

    @Override
    protected void onBindDialogView(View view)
    {
        super.onBindDialogView(view);

        mTimePicker = (TimePicker) view.findViewById(R.id.preferences_timepicker);

        if (mTimePicker == null)
        {
            throw new IllegalStateException("Dialog view must contain a TimePicker with id 'preferences_timepicker'");
        }

        // Get the time from the related Preference
        Integer minutesAfterMidnight = null;
        TimePreference preference = (TimePreference) findPreferenceInHierarchy("pref_notify_time");
        minutesAfterMidnight = preference.getTime();

        // Set the time to the TimePicker
        if (minutesAfterMidnight != null)
        {
            int hours = minutesAfterMidnight / 60;
            int minutes = minutesAfterMidnight % 60;
            boolean is24hour = DateFormat.is24HourFormat(getContext());

            mTimePicker.setIs24HourView(is24hour);

            if (Build.VERSION.SDK_INT >= 23)
            {
                mTimePicker.setHour(hours);
                mTimePicker.setMinute(minutes);
            }
            else
            {
                mTimePicker.setCurrentHour(hours);
                mTimePicker.setCurrentMinute(minutes);
            }
        }
    }

    @Override
    protected void onDialogClosed(boolean positiveResult)
    {
        if (positiveResult)
        {
            // Get the current values from the TimePicker
            int hours;
            int minutes;
            if (Build.VERSION.SDK_INT >= 23)
            {
                hours = mTimePicker.getHour();
                minutes = mTimePicker.getMinute();
            }
            else
            {
                hours = mTimePicker.getCurrentHour();
                minutes = mTimePicker.getCurrentMinute();
            }

            // Generate value to save
            int minutesAfterMidnight = (hours * 60) + minutes;

            // Save the value
            TimePreference timePreference = (TimePreference) findPreferenceInHierarchy("pref_notify_time");

            // This allows the client to ignore the user value.
            if (timePreference.callChangeListener(minutesAfterMidnight))
            {
                // Save the value
                timePreference.setTime(minutesAfterMidnight);
            }
        }
    }
}

The preferences_timepicker_dialog.xml file is as follows:

...
<TimePicker 
    xmlns:android="http://schemas.android.com/apk/res/android"
    android:id="@+id/preferences_timepicker"
    android:layout_width="match_parent"
    android:layout_height="match_parent" />

The result is like the screenshot below. On a Moto G5 plus phone with Android 7.

Question: There should be two preferences. However, the custom DialogPreference is not showing in the settings list. What is going wrong here? Does the AppCompatActivity actually work with the 'native' DialogPreference?

The TimePreference class is actually instantiated from the preferences xml, that could be logged from the constructor. Also no compile time errors, no runtime errors.

回答1:

Finally I found a different approach that looks clean, tested and works on real devices from Android 4 until 7. The Preference is showing in the Preference screen.

Also, the TimePicker dialog is properly showing in landscape orientation. This is a problem on some devices. See

  • Android TimePicker not displayed well on landscape mode
  • TimePickerDialog widget in landscape mode (PreferenceScreen)

Steps are:

  • Include a general Preference item in the preferences xml file
  • Set a click listener on this Preference using onPreferenceTreeClick()
  • When this Preference is clicked, show a regular (not compatibility library) TimePickerDialog like described in the 'Pickers' guide (https://developer.android.com/reference/android/app/TimePickerDialog.html)
  • Save the time set on the TimePicker manually in the SharedPreferences

The preferences Activity:

...
public class SettingsActivity extends AppCompatActivity
{
    public static final String KEY_PREF_NOTIFY = "pref_notify";
    public static final String KEY_PREF_NOTIFY_TIME = "pref_notify_time";

    @Override
    protected void onCreate(Bundle savedInstanceState)
    {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_preferences);
    }
}

The layout file activity_preferences.xml:

...

    <android.support.v4.widget.NestedScrollView
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:scrollbars="vertical"
        app:layout_behavior="@string/appbar_scrolling_view_behavior">

        <fragment
            android:name="nl.waywayway.broodkruimels.SettingsFragment"
            android:id="@+id/settings_fragment"
            android:layout_width="match_parent"
            android:layout_height="match_parent" />

    </android.support.v4.widget.NestedScrollView>

The SettingsFragment class:

...
public class SettingsFragment extends PreferenceFragmentCompat
{
    Context mContext;

    @Override
    public void onCreatePreferences(Bundle savedInstanceState, String rootKey)
    {
        setPreferencesFromResource(R.xml.preferences, rootKey);
    }

    // The Context object of this fragment is only available when this fragment is 'attached', so set the Context object inside the onAttach() method
    @Override
    public void onAttach(Context context)
    {
        super.onAttach(context);
        mContext = context;
    }

    // This method sets the action of clicking the Preference
    @Override
    public boolean onPreferenceTreeClick(Preference preference)
    {
        switch (preference.getKey())
        {
            case SettingsActivity.KEY_PREF_NOTIFY_TIME:
                showTimePickerDialog(preference);
                break;
        }

        return super.onPreferenceTreeClick(preference);
    }

    private void showTimePickerDialog(Preference preference)
    {
        DialogFragment newFragment = new TimePickerFragment();
        newFragment.show(getFragmentManager(), "timePicker");
    }
}

The preferences.xml file:

...
<android.support.v7.preference.PreferenceScreen
    xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent">

    <android.support.v7.preference.SwitchPreferenceCompat
        android:key="pref_notify"
        android:title="@string/pref_notify"
        android:summary="@string/pref_notify_summ"
        android:defaultValue="false" />

    <android.support.v7.preference.Preference
        android:dependency="pref_notify"
        android:key="pref_notify_time"
        android:title="@string/pref_notify_time"
        android:summary="@string/pref_notify_time_summ"
        android:defaultValue="390" />

</android.support.v7.preference.PreferenceScreen>

The TimePickerFragment class, see the Android 'Pickers' guide (https://developer.android.com/guide/topics/ui/controls/pickers.html) for an explanation:

...
public class TimePickerFragment extends DialogFragment
    implements TimePickerDialog.OnTimeSetListener
{
    private Context mContext;
    private int mTime; // The time in minutes after midnight

    // The Context object for this fragment is only available when this fragment is 'attached', so set the Context object inside the onAttach() method
    @Override
    public void onAttach(Context context)
    {
        super.onAttach(context);
        mContext = context;
    }

    // Getter and setter for the time
    public int getTime()
    {
        SharedPreferences sharedPref = PreferenceManager.getDefaultSharedPreferences(mContext);
        int prefDefault = mContext.getResources().getInteger(R.integer.preferences_time_default);
        mTime = sharedPref.getInt(SettingsActivity.KEY_PREF_NOTIFY_TIME, prefDefault);

        return  mTime;
    }

    public void setTime(int time)
    {
        mTime = time;

        // Save to Shared Preferences
        SharedPreferences sharedPref = PreferenceManager.getDefaultSharedPreferences(mContext);
        SharedPreferences.Editor editor = sharedPref.edit();
        editor.putInt(SettingsActivity.KEY_PREF_NOTIFY_TIME, time);
        editor.commit();
    }

    public Dialog onCreateDialog(Bundle savedInstanceState)
    {
        int minutesAfterMidnight = getTime();
        int hour = minutesAfterMidnight / 60;
        int minute = minutesAfterMidnight % 60;

        Log.i("HermLog", "onCreateDialog(), tijd: " + hour + ":" + minute);

        // Create a new instance of TimePickerDialog and return it
        return new TimePickerDialog(
            mContext, 
            this, 
            hour, 
            minute,
            DateFormat.is24HourFormat(mContext)
        );
    }

    @Override
    public void onTimeSet(TimePicker view, int hour, int minute)
    {
        int minutesAfterMidnight = (hour * 60) + minute;
        setTime(minutesAfterMidnight);      
    }
}