上一篇文章中我们从源码入手讲解了Preference画面展示的原理。这篇文章讲述下官方提供的Preference组件是怎么实现的,以及我们自己如何自定义Preference组件。
Preference UI分析
包括两部分。首先是组件本身的UI,然后是点击后展示的UI。
比如:
我们知道系统提供了不少Preference组件供我们使用,大体如下几种。
有些组件是针对组件本身的UI进行的定制,有些是针对点击后展示的UI进行的定制。
按照这种区别针对这些组件分为如下两类。
■定制Preference本身的UI
■定制点击后展示的UI
我们以相对复杂一点的VolumePreference为例,介绍下系统如何实现了自定义的音量调节设置组件。
VolumePreference分析
extends android.preference.SeekBarDialogPreference
extends android.preference.DialogPreference
extends android.preference.Preference
示例效果:
我们先来看下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。
比如点击设置项目后弹出下拉列表的DropdownPreference。
比如开发者选项画面里用来展示收集日志的BugreportPreference。
简单看下上述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的设置组件的目的。
DropdownPreference
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组件来复写将达到事半功倍的效果。