具有包含 TimePicker 的自定义本机(非兼容性)对话框首选项的 Appcompatactivity

Appcompatactivity with custom native (not compatibility) dialogpreference containing a TimePicker

我正在 Android AppCompatActivity 中构建首选项/设置屏幕。一个要求是带有 TimePicker.

的自定义 [DialogPreference][1]

DialogPreference 必须是 'native',这意味着不是 here and here 描述的兼容版本。

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);
    }
}

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>

SettingsFragment class:

...

public class SettingsFragment extends PreferenceFragment
{
    Context mContext;

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

preferences.xml 文件:

<?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>

和自定义 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);
            }
        }
    }
}

preferences_timepicker_dialog.xml文件如下:

...
<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" />

结果如下图所示。在 Moto G5 plus phone 上 Android 7.

问题: 应该有两个偏好。但是,自定义 DialogPreference 未显示在设置列表中。 这里出了什么问题? AppCompatActivity 是否真的与 'native' DialogPreference 一起工作?

TimePreference class 实际上是从首选项 xml 中实例化的,可以从构造函数中记录。也没有编译时错误,没有运行时错误。

最后我找到了一种不同的方法,它看起来很干净,经过测试并且可以在 Android 4 到 7 的真实设备上运行。首选项显示在首选项屏幕中。

此外,TimePicker 对话框可以正确地横向显示。这是某些设备上的问题。参见

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

步骤是:

首选项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);
    }
}

布局文件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>

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");
    }
}

preferences.xml 文件:

...
<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>

TimePickerFragmentclass,参见Android'Pickers'指南(https://developer.android.com/guide/topics/ui/controls/pickers.html)的解释:

...
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);      
    }
}