Preference元件探究之自定義Preference
上一篇文章中我們從原始碼入手講解了Preference畫面展示的原理。這篇文章講述下官方提供的Preference元件是怎麼實現的,以及我們自己如何自定義Preference元件。
Preference UI分析
包括兩部分。首先是元件本身的UI,然後是點選後展示的UI。
比如:
我們知道系統提供了不少Preference元件供我們使用,大體如下幾種。
有些元件是針對元件本身的UI進行的定製,有些是針對點選後展示的UI進行的定製。
按照這種區別針對這些元件分為如下兩類。
TwoStatePreference | AbstractClass,不能直接使用,需要自定義子類才能使用 |
CheckBoxPreference | 繼承自TwoStatePreference,展示外掛為核取方塊的設定專案 |
SwitchPreference | 繼承自TwoStatePreference,展示外掛為開關的設定專案 |
SeekBarPreference | 展示拖動條的設定專案 |
DialogPreference | AbstractClass,不能直接使用,需要自定義子類才能使用 |
EditTextPreference | 繼承自DialogPreference,點選後展示內嵌輸入框的對話方塊的設定專案 |
ListPreference | 繼承自DialogPreference,點選後展示內嵌ListView的對話方塊的設定專案 |
MultiSelectListPreference | 繼承自DialogPreference,點選後展示內嵌核取方塊的對話方塊的設定專案 |
SeekBarDialogPreference | 繼承自DialogPreference,點選後展示內嵌拖動條的對話方塊的設定專案 |
VolumePreference | 繼承自SeekBarDialogPreference,點選後展示音量大小拖動條的對話方塊的設定專案 |
RingtonePreference | 覆寫了點選處理,點選後跳轉到系統鈴聲設定頁面的設定專案 |
我們以相對複雜一點的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元件來複寫將達到事半功倍的效果。
相關文章
- PbRL | Preference Transformer:反正感覺 transformer 很強大ORM
- TornadoFx設定儲存功能((config和preference使用))
- RLHF · PBRL | PEBBLE:透過 human preference 學習 reward model
- Android Preference 設定看圖說話之基礎篇(5分鐘掌握)Android
- PbRL | Christiano 2017 年的開山之作,以及 Preference PPO / PrefPPO
- Flutter 之 自定義控制元件Flutter控制元件
- 自定義元件元件
- 擴充spring元件之自定義標籤Spring元件
- 微信開發之自定義元件(Toast)元件AST
- 自定義元件——TitleView元件View
- offline RL · PbRL | LiRE:構造 A>B>C 的 RLT 列表,得到更多 preference 資料
- Flutter實戰之自定義日誌列印元件Flutter元件
- Flutter動畫之自定義動畫元件-FlutterLayoutFlutter動畫元件
- Vue自定義元件之v-model的使用Vue元件
- vue 自定義元件tabbarVue元件tabBar
- Android 自定義UI元件AndroidUI元件
- Flutter自定義元件-MultiShowerFlutter元件
- 自定義元件-樣式元件
- angular自定義元件-UI元件篇-switch元件Angular元件UI
- vue框架之自定義元件中使用v-modelVue框架元件
- springcloud之自定義簡易消費服務元件SpringGCCloud元件
- 自定義View之簽到足跡控制元件View控制元件
- vue自定義全域性元件(或自定義外掛)Vue元件
- 自定義元件-元件的建立和引用元件
- 自定義元件-元件的生命週期元件
- svelte元件:Svelte3自定義Navbar+Tabbr元件|svelte自定義外掛元件
- 關於eclipse慕名奇妙找不到maven了,preference裡邊maven意外消失,解決方法EclipseMaven
- 微信小程式之如何使用自定義元件封裝原生 image 元件微信小程式元件封裝
- 4. 自定義控制元件(4) --- 自定義屬性控制元件
- 自定義控制元件ViewPager控制元件Viewpager
- Mint-UI 自定義元件UI元件
- Laravel 自定義檢視元件Laravel元件
- 自定義Switch控制元件控制元件
- Vue——關於自定義元件Vue元件
- vue 自定義報警元件Vue元件
- uniapp 自定義彈窗元件APP元件
- 微信小程式之自定義倒數計時元件微信小程式元件
- DRF內建認證元件之自定義認證系統元件