Preference组件探究之自定义Preference

简介: Preference组件探究之自定义Preference

上一篇文章中我们从源码入手讲解了Preference画面展示的原理。这篇文章讲述下官方提供的Preference组件是怎么实现的,以及我们自己如何自定义Preference组件。

Preference UI分析

包括两部分。首先是组件本身的UI,然后是点击后展示的UI。

比如:

1832b220aa754cd18c504acc7686a560.png我们知道系统提供了不少Preference组件供我们使用,大体如下几种。

1832b220aa754cd18c504acc7686a560.png

有些组件是针对组件本身的UI进行的定制,有些是针对点击后展示的UI进行的定制。

按照这种区别针对这些组件分为如下两类。

■定制Preference本身的UI

1672143554474.png

■定制点击后展示的UI

1672143572949.png

我们以相对复杂一点的VolumePreference为例,介绍下系统如何实现了自定义的音量调节设置组件。

VolumePreference分析

extends android.preference.SeekBarDialogPreference

 extends android.preference.DialogPreference

  extends android.preference.Preference


示例效果:

1832b220aa754cd18c504acc7686a560.png

我们先来看下Dialog弹出怎么实现的。

DialogPreference

public abstract class DialogPreference extends Preference…{
    public DialogPreference(
            Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
        super(context, attrs, defStyleAttr, defStyleRes);
        final TypedArray a = context.obtainStyledAttributes(attrs,
                com.android.internal.R.styleable.DialogPreference, defStyleAttr, defStyleRes);
        mDialogLayoutResId = a.getResourceId(com.android.internal.R.styleable.DialogPreference_dialogLayout,
                mDialogLayoutResId); // 从attr中读取布局ID。
        a.recycle();
    }
    // 覆写onClick逻辑调用展示Dialog
    protected void onClick() {
        if (mDialog != null && mDialog.isShowing()) return;
        showDialog(null);
    }
    protected void showDialog(Bundle state) {
        // 创建Dialog并显示
        mBuilder = new AlertDialog.Builder(context)
            .setTitle(mDialogTitle)
            .setIcon(mDialogIcon)
            .setPositiveButton(mPositiveButtonText, this)
            .setNegativeButton(mNegativeButtonText, this);
        // 创建Dialog的内容View
        View contentView = onCreateDialogView();
        if (contentView != null) {
            onBindDialogView(contentView); // 内容View的初始化
            mBuilder.setView(contentView);
        } else {
            mBuilder.setMessage(mDialogMessage);
        }    
    }
    // 加载配置的dialog布局
    // 可由dialogLayout标签或setDialogLayoutResource()指定
    protected View onCreateDialogView() {
        LayoutInflater inflater = LayoutInflater.from(mBuilder.getContext());
        return inflater.inflate(mDialogLayoutResId, null);
    }
    // 用以准备Dialog的View视图,进行一些配置,子类可覆写更改UI
    protected void onBindDialogView(View view) {
        View dialogMessageView = view.findViewById(com.android.internal.R.id.message);
    } }

那么SeekBar又是如何配置进去的呢。

SeekBarDialogPreference

public class SeekBarDialogPreference extends DialogPreference {
    public SeekBarDialogPreference(Context context, AttributeSet attrs) {
        // 指定了名为seekBarDialogPreferenceStyle的默认attr给父类的构造函数
        this(context, attrs, R.attr.seekBarDialogPreferenceStyle);★
    }
}

★处指定的默认attr如下。

<!-- frameworks/base/core/res/res/values/themes.xml-->
<style name="Theme">
    <item name="seekBarDialogPreferenceStyle">@style/Preference.DialogPreference.SeekBarPreference</item>
</style>

该默认的attr中dialogLayout标签指定的layout如下。

<!--frameworks/base/core/res/res/values/styles.xml-->
<style name="Preference.DialogPreference.SeekBarPreference">
    <item name="dialogLayout">@layout/preference_dialog_seekbar</item>
</style>
<!--frameworks/base/core/res/res/layout/preference_dialog_seekbar.xml-->
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
              android:layout_width="match_parent"
              android:layout_height="match_parent"
              android:gravity="center_horizontal"
              android:orientation="vertical">
    <ImageView
        android:id="@+id/icon"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:paddingTop="20dp" />
    <!--此处指定了包含SeekBar控件的布局-->
    <SeekBar
        android:id="@+id/seekbar"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:padding="20dp" />
</LinearLayout>

如果APP没有在style中,布局中以及setDialogLayoutResource()中复写dialog的layout ID的话,那么DialogPreference构造函数将从默认的attr里将上述包含SeekBar的布局加载进去。

最后又是怎么和音量产生关联的呢?

VolumePreference

public class VolumePreference extends SeekBarDialogPreference… {
    public VolumePreference(Context context, AttributeSet attrs) {
        // 指定的默认attr和父类一致,因为UI上它和父类完全相同
        this(context, attrs, R.attr.seekBarDialogPreferenceStyle);
    }
    protected void onBindDialogView(View view) {
        // 将SeekBar控件和SeekBarVolumizer组件产生关联
        // 并启动SeekBarVolumizer
        final SeekBar seekBar = (SeekBar) view.findViewById(R.id.seekbar);
        mSeekBarVolumizer = new SeekBarVolumizer(getContext(), mStreamType, null, this);
        mSeekBarVolumizer.start();
        mSeekBarVolumizer.setSeekBar(seekBar);
        // 设置KEY操作监听器并将SeekBar获取的焦点便于快速支持KEY处理
        view.setOnKeyListener(this);
        view.setFocusableInTouchMode(true);
        view.requestFocus();
    }
    public boolean onKey(View v, int keyCode, KeyEvent event) {
        // 监听硬件的音量+,-和静音键并向SeekBarVolumizer反映
        boolean isdown = (event.getAction() == KeyEvent.ACTION_DOWN);
        switch (keyCode) {
            case KeyEvent.KEYCODE_VOLUME_DOWN:
                if (isdown) {
                    mSeekBarVolumizer.changeVolumeBy(-1);
                }
                return true;
            case KeyEvent.KEYCODE_VOLUME_UP:
                if (isdown) {
                    mSeekBarVolumizer.changeVolumeBy(1);
                }
                return true;
            case KeyEvent.KEYCODE_VOLUME_MUTE:
                if (isdown) {
                    mSeekBarVolumizer.muteVolume();
                }
                return true;
        }
    }
    // Dialog取消或者意外关闭(非OK BTN)的场合
    protected void onDialogClosed(boolean positiveResult) {
        super.onDialogClosed(positiveResult);
        if (!positiveResult && mSeekBarVolumizer != null) {
            mSeekBarVolumizer.revertVolume(); // 将已设置回滚
        }
        cleanup();
    }
    // Activity或者Fragment的onStop回调进入后台的时候执行
    public void onActivityStop() {
        if (mSeekBarVolumizer != null) {
            mSeekBarVolumizer.stopSample(); // 将预览的铃声播发停止
        }
    }
    // 处理一些意外状况,将SeekBarVolumizer重置,线程结束等
    private void cleanup() {
       getPreferenceManager().unregisterOnActivityStopListener(this);
       if (mSeekBarVolumizer != null) {
           final Dialog dialog = getDialog();
           if (dialog != null && dialog.isShowing()) {
               final View view = dialog.getWindow().getDecorView().findViewById(R.id.seekbar);
               if (view != null) {
                   view.setOnKeyListener(null);
               }
               // Stopped while dialog was showing, revert changes
               mSeekBarVolumizer.revertVolume();
           }
           mSeekBarVolumizer.stop();
           mSeekBarVolumizer = null;
       }
    }
    // SeekBarVolumizer中铃声预览播放时候的回调,供APP处理
    public void onSampleStarting(SeekBarVolumizer volumizer) {
        if (mSeekBarVolumizer != null && volumizer != mSeekBarVolumizer) {
            mSeekBarVolumizer.stopSample();
        }
    }
    // SeekBar上的拖动条数值发生变化时候的回调,供APP知晓程度
    public void onProgressChanged(SeekBar seekBar, int progress, boolean fromTouch) {
        // noop
    }
    // 外部导致系统音量发生变化的回调
    public void onMuted(boolean muted, boolean zenMuted) {
        // noop
    }
}

至此,VolumePreference就是继承自SeekBarDialogPreference实现展示带SeekBar的dialog的组件。内部通过SeekBarVolumizer类去控制音量的设置,预览,回滚,保存和恢复等处理。


有必要提及下SeekBarVolumizer的处理细节。

SeekBarVolumizer

public class SeekBarVolumizer implements OnSeekBarChangeListener, Handler.Callback {
    // 持有SeekBar实例并监听拖动条进度
    public void setSeekBar(SeekBar seekBar) {
        if (mSeekBar != null) {
            mSeekBar.setOnSeekBarChangeListener(null);
        }
        mSeekBar = seekBar;
        mSeekBar.setOnSeekBarChangeListener(null);
        mSeekBar.setMax(mMaxStreamVolume);
        updateSeekBar();
        mSeekBar.setOnSeekBarChangeListener(this);
    }
    // 更新SeekBar进度
    protected void updateSeekBar() {
        final boolean zenMuted = isZenMuted();
        mSeekBar.setEnabled(!zenMuted);
        if (zenMuted) {
            mSeekBar.setProgress(mLastAudibleStreamVolume, true);
        } else if (mNotificationOrRing && mRingerMode == AudioManager.RINGER_MODE_VIBRATE) {
            mSeekBar.setProgress(0, true);
        } else if (mMuted) {
            mSeekBar.setProgress(0, true);
        } else {
            mSeekBar.setProgress(mLastProgress > -1 ? mLastProgress : mOriginalStreamVolume, true);
        }
    }
    // 音量调节逻辑开始,由Preference调用
    public void start() {
        if (mHandler != null) return;  // already started
        // 启动工作Thread
        HandlerThread thread = new HandlerThread(TAG + ".CallbackHandler");
        thread.start();
        // 创建该Thread的Handler并在该线程里初始化铃声播放器实例
        mHandler = new Handler(thread.getLooper(), this);
        mHandler.sendEmptyMessage(MSG_INIT_SAMPLE);
        // 监听系统音量的变化,变化交由上述线程的Handler处理
        mVolumeObserver = new Observer(mHandler);
        mContext.getContentResolver().registerContentObserver(
                System.getUriFor(System.VOLUME_SETTINGS[mStreamType]),
                false, mVolumeObserver);
        // 监听系统音量,铃声模式变化的广播
        mReceiver.setListening(true);
    }
    //音量调节逻辑结束,由Preference调用
    public void stop() {
        if (mHandler == null) return;  // already stopped
        postStopSample(); // 关闭铃声播放
        // 注销内容监听,广播监听,Thread内Looper停止轮询消息等重置处理
        mContext.getContentResolver().unregisterContentObserver(mVolumeObserver);
        mReceiver.setListening(false);
        mSeekBar.setOnSeekBarChangeListener(null);
        mHandler.getLooper().quitSafely();
        mHandler = null;
        mVolumeObserver = null;
    }
    // 运行在工作线程的Handler回调
    public boolean handleMessage(Message msg) {
        switch (msg.what) {
            case MSG_SET_STREAM_VOLUME:
                break;
            case MSG_START_SAMPLE:
                onStartSample();
                break;
            case MSG_STOP_SAMPLE:
                onStopSample();
                break;
            case MSG_INIT_SAMPLE:
                onInitSample();
                break;
            default:
                Log.e(TAG, "invalid SeekBarVolumizer message: "+msg.what);
        }
        return true;
    }
    // 初始化铃声播放,运行在工作Thread中
    private void onInitSample() {
        synchronized (this) {
            mRingtone = RingtoneManager.getRingtone(mContext, mDefaultUri);
            if (mRingtone != null) {
                mRingtone.setStreamType(mStreamType);
            }
        }
    }
    // 通知工作Thread需要开始播放
    private void postStartSample() {
        if (mHandler == null) return;
        mHandler.removeMessages(MSG_START_SAMPLE);
        mHandler.sendMessageDelayed(mHandler.obtainMessage(MSG_START_SAMPLE),
                isSamplePlaying() ? CHECK_RINGTONE_PLAYBACK_DELAY_MS : 0);
    }
    // 工作Thread响应开始播放
    private void onStartSample() {
        if (!isSamplePlaying()) {
            // 执行Preference的回调
            if (mCallback != null) {
                mCallback.onSampleStarting(this);
            }
            synchronized (this) {
                if (mRingtone != null) {
                    try {
                        mRingtone.setAudioAttributes(new AudioAttributes.Builder(mRingtone
                                .getAudioAttributes())
                                .setFlags(AudioAttributes.FLAG_BYPASS_MUTE)
                                .build());
                        mRingtone.play();
                    } catch (Throwable e) {
                        Log.w(TAG, "Error playing ringtone, stream " + mStreamType, e);
                    }
                }
            }
        }
    }
    // 通知工作Thread停止播放
    private void postStopSample() {
        if (mHandler == null) return;
        // remove pending delayed start messages
        mHandler.removeMessages(MSG_START_SAMPLE);
        mHandler.removeMessages(MSG_STOP_SAMPLE);
        mHandler.sendMessage(mHandler.obtainMessage(MSG_STOP_SAMPLE));
    }
    // 工作Thread相应停止播放
    private void onStopSample() {
        synchronized (this) {
            if (mRingtone != null) {
                mRingtone.stop();
            }
        }
    }
    // UI线程的进度变化后处理,通知工作线程音量发生变化
    public void onProgressChanged(SeekBar seekBar, int progress, boolean fromTouch) {
        if (fromTouch) {
            postSetVolume(progress);
        }
        // 回调Preference的处理
        if (mCallback != null) {
            mCallback.onProgressChanged(seekBar, progress, fromTouch);
        }
    }
    // 向工作线程发出通知
    private void postSetVolume(int progress) {
        if (mHandler == null) return;
        // Do the volume changing separately to give responsive UI
        mLastProgress = progress;
        mHandler.removeMessages(MSG_SET_STREAM_VOLUME);
        mHandler.sendMessage(mHandler.obtainMessage(MSG_SET_STREAM_VOLUME));
    }
    // 开始拖动不作处理(音量变化由onProgressChanged通工作知线程去更新音量值)
    public void onStartTrackingTouch(SeekBar seekBar) {
    }
    // 拖动停止时开始处理通知工作线程播放,因为需要预览暂时设置好的音量效果
    public void onStopTrackingTouch(SeekBar seekBar) {
        postStartSample();
    }
    // 预留了供APP调用用于手动预览音量效果和停止预览的接口
    public void startSample() {
        postStartSample();
    }
    public void stopSample() {
        postStopSample();
    }
    // 供APP调用用于逐格调节音量的接口,比如系统的Volume+-按钮触发
    // 将通知工作线程设置音量和播放效果
    public void changeVolumeBy(int amount) {
        mSeekBar.incrementProgressBy(amount);
        postSetVolume(mSeekBar.getProgress());
        postStartSample();
        mVolumeBeforeMute = -1;
    }
    // 供APP调用用于设置是否静音的接口,比如系统的静音按钮触发
    // 将通知工作线程设置音量和播放效果
    public void muteVolume() {
        if (mVolumeBeforeMute != -1) {
            mSeekBar.setProgress(mVolumeBeforeMute, true);
            postSetVolume(mVolumeBeforeMute);
            postStartSample();
            mVolumeBeforeMute = -1;
        } else {
            mVolumeBeforeMute = mSeekBar.getProgress();
            mSeekBar.setProgress(0, true);
            postStopSample();
            postSetVolume(0);
        }
    }
    // 定义在UI线程的Handler,用于更新SeekBar进度
    private final class H extends Handler {
        private static final int UPDATE_SLIDER = 1;
        @Override
        public void handleMessage(Message msg) {
            if (msg.what == UPDATE_SLIDER) {
                if (mSeekBar != null) {
                    mLastProgress = msg.arg1;
                    mLastAudibleStreamVolume = msg.arg2;
                    final boolean muted = ((Boolean)msg.obj).booleanValue();
                    if (muted != mMuted) {
                        mMuted = muted;
                        if (mCallback != null) {
                            mCallback.onMuted(mMuted, isZenMuted());
                        }
                    }
                    updateSeekBar();
                }
            }
        }
        public void postUpdateSlider(int volume, int lastAudibleVolume, boolean mute) {
            obtainMessage(UPDATE_SLIDER, volume, lastAudibleVolume, new Boolean(mute)).sendToTarget();
        }
    }
    // 通知UI线程更新SeekBar
    private void updateSlider() {
        if (mSeekBar != null && mAudioManager != null) {
            final int volume = mAudioManager.getStreamVolume(mStreamType);
            final int lastAudibleVolume = mAudioManager.getLastAudibleStreamVolume(mStreamType);
            final boolean mute = mAudioManager.isStreamMute(mStreamType);
            mUiHandler.postUpdateSlider(volume, lastAudibleVolume, mute);
        }
    }
    // 监听到系统音量变化通知UI线程刷新
    private final class Observer extends ContentObserver {
        public Observer(Handler handler) {
            super(handler);
        }
        @Override
        public void onChange(boolean selfChange) {
            super.onChange(selfChange);
            updateSlider();
        }
    }
    // 监听音量变化广播,必要时向UI线程发送刷新请求
    private final class Receiver extends BroadcastReceiver {
        public void onReceive(Context context, Intent intent) {
            …else if (AudioManager.STREAM_DEVICES_CHANGED_ACTION.equals(action)) {
                int streamType = intent.getIntExtra(AudioManager.EXTRA_VOLUME_STREAM_TYPE, -1);
                int streamVolume = mAudioManager.getStreamVolume(streamType);
                updateVolumeSlider(streamType, streamVolume);
            } else if (NotificationManager.ACTION_INTERRUPTION_FILTER_CHANGED.equals(action)) {
                mZenMode = mNotificationManager.getZenMode();
                updateSlider();
            }
        }
    }
}

总结上述过程。

总结上述过程。

SeekBarVolumizer的作用是将SeekBar和音量设置产生关联,让UI上的展示和设置的数值保持一致。

SeekBar上拖动条拖动或按键触发的音量调节由SeekBarVolumizer经Handler向工作线程发出数值更新,播放和停止的请求。

SeekBarVolumizer监听系统音量,铃声的设置经Handler向UI线程发出UI刷新的请求。



除了系统公开的Preference组件外,系统Settings APP也自定了不少组件。

Settings自定义Preference分析

比如:

移动/Wi-Fi使用量画面展示数据使用图表的ChartDataUsagePreference。

1832b220aa754cd18c504acc7686a560.png

比如点击设置项目后弹出下拉列表的DropdownPreference。

1832b220aa754cd18c504acc7686a560.png

比如开发者选项画面里用来展示收集日志的BugreportPreference。

1832b220aa754cd18c504acc7686a560.png

简单看下上述Preference是如何自定义的。

ChartDataUsagePreference

public class ChartDataUsagePreference extends Preference {
    public ChartDataUsagePreference(Context context, AttributeSet attrs) {
        // 指定包含图表UsageView的自定义布局
        setLayoutResource(R.layout.data_usage_graph); 
    }
    // 采用的是support包的Preference
    // 覆写了类似onBindView()的onBindViewHolder()
    // 针对自定义布局内的UsageView做些初始化处理
    public void onBindViewHolder(PreferenceViewHolder holder) {
        super.onBindViewHolder(holder);
        UsageView chart = (UsageView) holder.findViewById(R.id.data_usage);
        if (mNetwork == null) return;
        int top = getTop();
        chart.clearPaths();
        chart.configureGraph(toInt(mEnd - mStart), top);
        calcPoints(chart);
        chart.setBottomLabels(new CharSequence[] {
                Utils.formatDateRange(getContext(), mStart, mStart),
                Utils.formatDateRange(getContext(), mEnd, mEnd),
        });
        bindNetworkPolicy(chart, mPolicy, top);
    }
    // 根据系统的NetworkPolicy接口设置图表的属性
    private void bindNetworkPolicy(UsageView chart, NetworkPolicy policy, int top) {
        if (policy.limitBytes != NetworkPolicy.LIMIT_DISABLED) {
            topVisibility = mLimitColor;
            labels[2] = getLabel(policy.limitBytes, R.string.data_usage_sweep_limit, mLimitColor);
        }
        if (policy.warningBytes != NetworkPolicy.WARNING_DISABLED) {
            chart.setDividerLoc((int) (policy.warningBytes / RESOLUTION));
            float weight = policy.warningBytes / RESOLUTION / (float) top;
            float above = 1 - weight;
            chart.setSideLabelWeights(above, weight);
            middleVisibility = mWarningColor;
            labels[1] = getLabel(policy.warningBytes, R.string.data_usage_sweep_warning,
                    mWarningColor);
        }
        chart.setSideLabels(labels);
        chart.setDividerColors(middleVisibility, topVisibility);
    }
}

总结:ChartDataUsagePreference指定包含图表UsageView的自定义布局替换系统默认的Preference布局,并通过业务相关的NetworkPolicy接口获取数据去填充图表达到展示独特Ui的设置组件的目的。

public class DropDownPreference extends ListPreference {
    private Spinner mSpinner; // 内部持有Spinner实例
    public DropDownPreference(Context context, AttributeSet attrs, int defStyleAttr,
            int defStyleRes) {
        super(context, attrs, defStyleAttr, defStyleRes);
        mContext = context;
        mAdapter = createAdapter(); // 创建Spinner用的Adapter
        updateEntries();
    }
    // 复写父类方法指定更改了布局的Adapter实例
    protected ArrayAdapter createAdapter() {
        return new ArrayAdapter<>(mContext, android.R.layout.simple_spinner_dropdown_item);
    }
    protected void onClick() {
        mSpinner.performClick(); // Spinner处理点击事件
    }
    // 复写父类的数据源往Adapter里填充
    public void setEntries(@NonNull CharSequence[] entries) {
        super.setEntries(entries);
        updateEntries();
    }
    private void updateEntries() {
        mAdapter.clear();
        if (getEntries() != null) {
            for (CharSequence c : getEntries()) {
                mAdapter.add(c.toString());
            }
        }
    }
    // 复写数据更新回调,通知Spinner刷新
    protected void notifyChanged() {
        super.notifyChanged();
        mAdapter.notifyDataSetChanged();
    }
    // 复写绑定逻辑,将Spinner和数据绑定
    public void onBindViewHolder(PreferenceViewHolder view) {
        mSpinner = (Spinner) view.itemView.findViewById(R.id.spinner);
        mSpinner.setAdapter(mAdapter);
        mSpinner.setOnItemSelectedListener(mItemSelectedListener);
        // 设置Spinner初始选中项目
mSpinner.setSelection(findSpinnerIndexOfValue(getValue()));
        super.onBindViewHolder(view);
    }
    // 监听Spinner点击事件,将设置保存
    private final OnItemSelectedListener mItemSelectedListener = new OnItemSelectedListener() {
        @Override
        public void onItemSelected(AdapterView<?> parent, View v, int position, long id) {
            if (position >= 0) {
                String value = getEntryValues()[position].toString();
                if (!value.equals(getValue()) && callChangeListener(value)) {
                    setValue(value);
                }
            }
        }
    };
}

总结

DropdownPreference定制的是点击后UI变为Spinner,本身的UI和一般的Preference并没有什么区别。

而ListPreference是系统提供的点击后弹出带ListView的对话框的Preference,和上述的定制需求类似。

所以AOSP选择继承自ListPreference并复写click事件将处理由dialog弹出变为Spinner的弹出。同时将复写了其他函数将数据的绑定切换为针对Spinner的数据处理。


注意:事实上这个Preference还是更改了本身的布局的。构造函数里指定了dropdownPreferenceStyle的默认attr,该attr将会指定一个包含Spinner控件的布局。只不过在布局里将Spinner设置为隐藏,导致该Preference和普通Preference并无明显区别。

BugreportPreference

public class BugreportPreference extends CustomDialogPreference {
    protected void onPrepareDialogBuilder(Builder builder, DialogInterface.OnClickListener listener) {
        // 指定自定义Dialog的布局
        final View dialogView = View.inflate(getContext(), R.layout.bugreport_options_dialog, null);
        // 监听采集LOG选项的点击事件
        final View.OnClickListener l = new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                if (v == mFullTitle || v == mFullSummary) {
                    mInteractiveTitle.setChecked(false);
                    mFullTitle.setChecked(true);
                }
                if (v == mInteractiveTitle || v == mInteractiveSummary) {
                    mInteractiveTitle.setChecked(true);
                    mFullTitle.setChecked(false);
                }
            }
        };
        mInteractiveTitle.setOnClickListener(l);
        mFullTitle.setOnClickListener(l);
        mInteractiveSummary.setOnClickListener(l);
        mFullSummary.setOnClickListener(l);
        builder.setPositiveButton(com.android.internal.R.string.report, listener);
        builder.setView(dialogView);
    }
    // 复写Dialog点击事件,OK的情况下按需调用采集LOG处理
    protected void onClick(DialogInterface dialog, int which) {
        if (which == DialogInterface.BUTTON_POSITIVE) {
            final Context context = getContext();
            if (mFullTitle.isChecked()) {
                Log.v(TAG, "Taking full bugreport right away");
                FeatureFactory.getFactory(context).getMetricsFeatureProvider().action(context,
                        MetricsEvent.ACTION_BUGREPORT_FROM_SETTINGS_FULL);
                takeBugreport(ActivityManager.BUGREPORT_OPTION_FULL);
            }…
        }
    }
    // 封装的调用系统采集LOG函数
    private void takeBugreport(int bugreportType) {
        try {
            ActivityManager.getService().requestBugReport(bugreportType);
        } catch (RemoteException e) {
            Log.e(TAG, "error taking bugreport (bugreportType=" + bugreportType + ")", e);
        }
    }
}

总结

BugreportPreference通过继承自CustomDialogPreference复写布局和监听逻辑达到展示采集LOG设置条目的目的。



上述分类,分析并总结了典型的系统及Settings APP提供的自定义Preference组件,使我们对于自定义原理有了清晰的了解。


我们整理归纳下自定义Preference的方法。

自定义Preference方法

■指定style法

定义一个指定了布局的style给Activity。

比如:

<style name="MyTheme">
    <item name="preferenceStyle">@style/MyPreferenceStyle</item>
</style>
<style name=" MyPreferenceStyle">
    <item name="android:layout">@layout/my_preference_layout</item>
</style>

备注:

其实不止Preference,像PreferenceFragment,PreferenceScreen,EditTextPreference等都有属于自己的sytle用的attr。通过官网或者源码找到对应的attr名称,APP可以灵活指定自己的style。

■布局或者JAVA调用法

在Preference布局里利用layout标签或者调用setLayoutResource()去指定自己的布局。

比如:

<PreferenceScreen>
    <Preference
        android:layout=”@layout/my_preference_layout”
/>
</PreferenceScreen>
myPreferenceInstance.setLayoutResource(R.layout. my_preference_layout);

以上两种方法只适用于简单的UI定制,无法适用于复杂场景或者UI改动较大的需求。

■复写系统Preference组件灵活定制

public class MyPreference extends Preference {
// 复写必要的构造函数。
    // 用于布局里使用该Preference时使用
    public MyPreference(Context context, AttributeSet attrs) {
        // 可以参考父类指定默认的attr名
        // 也可以指定自定义的attr,为方便APP在xml的灵活配置
        this(context, attrs, xxx); 
    }
    // 用于Java里手动创建Preference时使用
    public DialogPreference(Context context) {
        this(context, null);
    }
// 复写必要的View绑定逻辑
    // 继承自base包下Preference时使用
    protected void onBindView(View view) {
    }
    // 继承自support包下Preference时使用
    public void onBindViewHolder(PreferenceViewHolder view) {
    }
// 复写点击事件(如果需要定制点击处理的话)
    protected void onClick() {
    }
// 复写一些特定的父类的处理(如果由需要的话)
    // 比如SeekbarDialogPreference需要将dialog布局内icon隐藏
    protected void onBindDialogView(View view) {
    }
    ...
}

在实际的开发过程中,我们可以根据业务需求去寻找现成的Preference组件,避免重复造轮子。


如果没有现成的,考虑通过style或者java简单定制是否可以达到目的。


最后只能通过继承复写的方法精准达到我们的目的,当然选择类似要求的已有Preference组件来复写将达到事半功倍的效果。


相关文章
|
Android开发 开发者 存储
Android可折叠收缩伸展的Expandable分组RecyclerView:模型抽象和封装(二)
Android可折叠收缩伸展的Expandable分组RecyclerView:模型抽象和封装(二) 如今Android这种可收缩伸展的Expandable RecyclerView是如此常用,在附录1的基础上,我重新把模型进行了抽象和封装,设计了一套新的架构和简洁的使用方式支持这种功能。
3835 0
|
XML Android开发 数据格式
Android6.0 Settings源码解析、配置项动态添加和静态添加
Android6.0 Settings源码解析、配置项动态添加和静态添加
235 0
|
XML Java Android开发
android10.0(Q) Settings 添加设置项————静态方式
android10.0(Q) Settings 添加设置项————静态方式
564 0
|
Android开发
Preference组件探究之使用说明
Preference组件探究之使用说明
Preference组件探究之使用说明
|
缓存 Java API
Preference组件探究之Base,Support及AndroidX对比
Preference组件探究之Base,Support及AndroidX对比
Preference组件探究之Base,Support及AndroidX对比
|
XML API Android开发
Preference组件探究之源码解读
Preference组件探究之源码解读
|
Java Android开发
【Android 组件化】路由组件 ( 注解处理器参数选项设置 )
【Android 组件化】路由组件 ( 注解处理器参数选项设置 )
216 0
【Android 组件化】路由组件 ( 注解处理器参数选项设置 )
|
Android开发 开发者
基于Android官方AsyncListUtil优化经典ListView分页加载机制(二)
基于Android官方AsyncListUtil优化经典ListView分页加载机制(二) 我写的附录文章1,介绍了如何使用Android官方的分页加载框架AsyncListUtil优化改进常见的RecyclerView分页加载实现。
1345 0