直观:Preference组件探究之自定义Preference
优采云 发布时间: 2022-11-01 03:25直观:Preference组件探究之自定义Preference
在前面的文章中,我们从源代码中解释了首选项屏幕显示的原理。本文章描述了官方引用组件是如何实现的,以及我们如何自己自定义 PReference 组件。
首选项 UI 分析
包括两部分。首先是组件本身的 UI,然后是单击时显示的 UI。
例如:
我们知道系统提供了很多 Ppreferences组件供我们使用,大致如下。
某些组件是为 UI 自定义的
组件本身,有些是针对单击后显示的 UI 自定义的。
根据这种区别,这些组件分为以下两类。
■ 自定义首选项本身的UI
双状态首选项
不能直接使用的 AbstractClass 需要自定义子类才能使用
复选框首选项
继承自 TwoStatePpreferences,它显示了复选框的插件设置项
开关首选项
继承自 TwoStatePpreferences,它将插件显示为开关设置项
搜索栏首选项
显示拖动栏的设置项
■ 自定义点击后显示的UI
对话框首选项
不能直接使用的 AbstractClass 需要自定义子类才能使用
编辑文本首选项
继承自“对话框首选项”,单击以显示带有嵌入输入框的对话框的设置项
列表首选项
继承自“对话框首选项”,单击以显示嵌入在列表视图中的对话框的设置项
多选列表首选项
继承自对话框首选项,对话框的设置项,单击时显示嵌入的复选框
搜索栏对话框首选项
继承自对话框首选项,单击后将显示带有嵌入式拖动栏的对话框的设置项
卷首选项
继承自 SeekBarDialogPpreferences,单击后显示音量级别拖动栏的对话框的设置项
铃声偏好
覆盖点击
处理中,点击跳转到系统铃声设置页面的设置项
让我们以相对复杂的音量偏好为例,介绍系统如何实现自定义音量调整设置组件。
体积参考分析
扩展 android.preference.SeekBarDialogPreference
扩展 android.preference.DialogPreference
扩展 android.ppreferences.Ppreferences
示例效果:
让我们先来看看对话框弹出窗口是如何工作的。
对话框首选项
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是如何适应的呢?
搜索栏对话框首选项
public class SeekBarDialogPreference extends DialogPreference {
…
public SeekBarDialogPreference(Context context, AttributeSet attrs) {
// 指定了名为seekBarDialogPreferenceStyle的默认attr给父类的构造函数
this(context, attrs, R.attr.seekBarDialogPreferenceStyle);★
}
…
}
★ 指定的默认属性如下所示。
@style/Preference.DialogPreference.SeekBarPreference
…
默认属性中的 dialogLayout 标记指定的布局如下所示。
@layout/preference_dialog_seekbar
如果应用未覆盖样式、布局和 setDialogLayoutResource() 中对话框的布局 ID,则 DialogPreference 构造函数将从默认属性加载收录 SeekBar 的布局。
最后,它与体积有什么关系?
卷首选项
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
}
…
}
此时,VolumePpreferences继承自SeekBarDialogPpreferences实现,以显示带有SeekBar的对话框组件。类在内部用于控制卷设置、预览、回滚、保存和还原。
值得一提的是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);
<p>
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();
}
}
}
}</p>
总结上述过程。
SeekBarVolumizer的作用是将SeekBar与音量设置相关联,以便UI上的显示和设置的值保持一致。
由 SeekBar Volumizer 通过处理程序请求通过拖动 SeekBar 上的条或键触发的音量调整,以更新值、播放和停止值。
SeekBarVolumizer 监视系统音量,铃声由处理程序设置,以向 UI 线程发送 UI 刷新请求。
除了系统公开的偏好组件外,系统设置APP还自定义了许多组件。
设置自定义首选项分析
例如:
移动/Wi-Fi 使用情况屏幕显示数据使用图表图表数据使用情况首选项。
例如,单击“下拉列表首选项”,该首选项将在设置项目后弹出下拉列表。
例如,开发人员选项屏幕用于显示用于采集日志的错误报告首选项。
快速浏览一下上述首选项是如何自定义的。
图表数据使用首选项
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);
}
…
}
摘要:图表数据使用偏好指定收录图表使用视图的自定义布局,以替换系统的默认首选项布局,并通过业务相关的网络策略接口获取数据以填充图表以显示唯一的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);
}
}
}
…
};
}
总结
下拉首选项自定义是 UI 在单击后变为微调器,它自己的 UI 与普通首选项没有什么不同。
ListReference是系统提供的参考,点击后弹出一个带有ListView的对话框,与上述自定义要求类似。
因此,AOSP 选择从 ListPreference 继承并覆盖单击事件将处理从对话框到微调器弹出窗口的弹出窗口。同时,其他函数的覆盖数据的绑定切换到Spinner的数据处理。
注: 实际上,此引用会更改其自己的布局。构造函数指定 dropdownPreferenceStyle 的默认属性,这将指定收录微调器控件的布局。只是将 Spinner 设置为隐藏在布局中,因此此引用与普通首选项没有显着差异。
错误报告首选项
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);
}
}
}
总结
BugreportPpreferences通过重写从CustomDialogPpreferences继承的布局和侦听逻辑来实现显示采集LOG设置条目的目的。
以上对典型系统以及设置 APP 提供的自定义偏好组件进行了分类、分析和总结,让我们对自定义原理有了清晰的认识。
让我们总结一下自定义首选项的方法。
自定义首选项方法 ■ 指定样式方法
为活动定义具有指定布局的样式。
例如:
@style/MyPreferenceStyle
@layout/my_preference_layout
言论:
事实上,不仅 Ppreferences,例如 PreferenceFragment、PreferenceScreen、EditTextPreference 等都有自己的 sytle 属性。通过官网或源码找到对应的attr名称,APP可以灵活指定自己的风格。
■ 布局或JAVA调用方法
使用布局
标记或调用 setLayoutResource() 来指定您自己的布局。
例如:
…
或
myPreferenceInstance.setLayoutResource(R.layout. my_preference_layout);
以上两种方法只适用于简单的UI自定义,不能应用于UI变化较大的复杂场景或需求。
■灵活定制复制系统的首选项组件
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) {
…
}
...
}
在实际开发过程中,我们可以根据业务需求寻找现成的 Ppreferred 组件,避免重建车轮。
如果没有现成的可用,请考虑通过样式或Java进行简单定制是否可以实现目标。
最后,我们只有继承了复制的方法,才能准确地达到我们的目的,当然,选择现有的具有类似需求的 Ppreferences组件进行复制,将达到事半功倍的效果。
成熟的解决方案:需求采集与研究
工作了七八年,职场给我最大的感悟就是创新就是未来,创新的前提是了解用户的需求。经过这么多年的编码和在网上的几年的沉浸,我总结了一些需求采集的方法>分享给大家。
1:需求分类采集>方法
采集>需求只是手段,目的是通过研究用户更好地满足需求
1.直接采集>和间接采集>——>主次需求
直接面对用户->一手需求;通过复述,第三方行业分析报告->二手需求
直接采集>:只有这样,产品本身和自己对产品的判断才能更接地气
间接采集>:需要考虑需求者和提议者是谁,是否被误解了
一般都是经过整理的,得出结论的效率更高。
2.说和做,定性和定量
用户访谈——《问卷调查》——《可用性测试》——《数据分析》
3. 是在真实场景中吗?
临场感,最好的需求是在真实场景的位置采集>
负面例子:
钓鱼智能浮标——实际用户钓鱼时不方便使用手机
地下车库寻车APP-无信号
4.与产品是否有互动
对于某些产品,用户想象中是否需要,与实际使用后是否需要完全不同。
电视需要,洗碗机需要
潜在原因:不可靠的用户
解决方案:先简化实现,或者用人肉跑流程来验证、尝试、租用
经典示例:免费试用
二:一些实用的采集>方法
2.1 腾讯10/100/1000法:产品经理每个月要做10次用户调查,阅读100篇用户写的文章文章,处理1000个用户反馈
2.2 如有条件,新入职人员可到客服部轮岗数周至数月,或听客服电话半个月。
2.3 利用互联网、微博、百度等搜索引擎的大数据,会告诉我们很多用户痛点
三:用户,重新认识需求
3.1 需求的三个深度
意见和行为
目标和动机(更现实)
人性与价值观:
马斯洛的需求层次——本我、自我、超我。
层次越低的需求越笼统、僵化、工具化,越难以发光
更高层次的需求更细分(场景、时间、空间)
3.2 如何理解这种内部需求的战略需求
如何理解内部需求与狭隘用户需求的关系?
一个。提议者是泛化用户,所以这些需求必须考虑,但要判断优先级
湾。分清目的和手段。通过服务外部用户实现商业价值,满足外部用户价值是最终目标。组建团队只是实现目标的一种手段。
为了满足自己的指标,伤害用户的 KPI:
提高优采云出勤率,改善北京空气质量,防止校车事故,防止学生在春节期间站着回家。
3.3 用户:从抽象到具体再到抽象
产品概念阶段,想象中的目标用户,核心用户;
求采集>舞台,看活人,听故事,找情;
在需求采集>之后,合并特性,定义角色,修改产品概念方向
工具:用户故事
两个典型用户:新手和专家
核心用户是典型的新手产品:电梯、公测、垃圾桶等用户量大的海量产品;电视、冰箱、洗衣机、2C产品
核心用户是专家的典型产品:少数用户,如专业仪器、乐器、单反、2B产品
四:产品原理及初衷
制定产品原则的前提是已经需要一个产品采集>并且资料充足
它是什么:整个产品团队必须达成一致的共识指南
元素:
1 目标用户分为几类,优先级是什么;
2 产品市场切入点最关键的场景是什么?最小可行产品必须满足哪些功能
3 一个产品要追求用户数量或用户质量,追求流量或利润,这些都必须收录在产品原则中。
4 人与内容哪个更重要?