先来看看APIDemo中的Preference是什么样子.
这里个效果实现起来挺简单的,不需要自己写布局文件,只需要在res/xml目录下配置一下自己的设置选项,简单的加载一下就可以实现.但是如果你想要改变一下样式,该怎么办呢?(不了解基本用法的请自行百度)
接下来看看一下有哪些属性值是可以修改的,我们可不可以通过修改系统提供的属性达到修改样式的效果呢?
<SwitchPreference
android:key="checkbox_preference"
android:title="@string/title_switch_preference"
android:summary="@string/summary_switch_preference_yes_no"
android:switchTextOn = "YES"
android:switchTextOff = "NO"
android:defaultValue="true" />
由于Preference是采用SharedPreference保存数据的,这里的属性key表示开关状态.defaultValue表示默认存储的值
我们发现并没有提供可供修改样式的属性,比如背景色,Switch的track属性,thumb属性.
接下里分析一下SwitchPreference的源代码:
继承结构如下
SwitchPreference->TwoStatePreference->Preference
首先SwitchPreference解析attrs
public SwitchPreference(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
super(context, attrs, defStyleAttr, defStyleRes);
TypedArray a = context.obtainStyledAttributes(attrs,
com.android.internal.R.styleable.SwitchPreference, defStyleAttr, defStyleRes);
setSummaryOn(a.getString(com.android.internal.R.styleable.SwitchPreference_summaryOn));
setSummaryOff(a.getString(com.android.internal.R.styleable.SwitchPreference_summaryOff));
setSwitchTextOn(a.getString(
com.android.internal.R.styleable.SwitchPreference_switchTextOn));
setSwitchTextOff(a.getString(
com.android.internal.R.styleable.SwitchPreference_switchTextOff));
setDisableDependentsState(a.getBoolean(
com.android.internal.R.styleable.SwitchPreference_disableDependentsState, false));
a.recycle();
}
可以看到,解析到到了诸如SummaryOff,SummaryOn等属性,并没有background的字眼.
接着看点到super(context, attrs, defStyleAttr, defStyleRes);中看看,这里直接进到了Preference中.
public Preference(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
mContext = context;
final TypedArray a = context.obtainStyledAttributes(
attrs, com.android.internal.R.styleable.Preference, defStyleAttr, defStyleRes);
for (int i = a.getIndexCount() - 1; i >= 0; i--) {
int attr = a.getIndex(i);
switch (attr) {
case com.android.internal.R.styleable.Preference_icon:
mIconResId = a.getResourceId(attr, 0);
break;
case com.android.internal.R.styleable.Preference_key:
mKey = a.getString(attr);
break;
case com.android.internal.R.styleable.Preference_title:
mTitleRes = a.getResourceId(attr, 0);
mTitle = a.getString(attr);
break;
case com.android.internal.R.styleable.Preference_summary:
mSummary = a.getString(attr);
break;
case com.android.internal.R.styleable.Preference_order:
mOrder = a.getInt(attr, mOrder);
break;
case com.android.internal.R.styleable.Preference_fragment:
mFragment = a.getString(attr);
break;
case com.android.internal.R.styleable.Preference_layout:
mLayoutResId = a.getResourceId(attr, mLayoutResId);
break;
case com.android.internal.R.styleable.Preference_widgetLayout:
mWidgetLayoutResId = a.getResourceId(attr, mWidgetLayoutResId);
break;
case com.android.internal.R.styleable.Preference_enabled:
mEnabled = a.getBoolean(attr, true);
break;
case com.android.internal.R.styleable.Preference_selectable:
mSelectable = a.getBoolean(attr, true);
break;
case com.android.internal.R.styleable.Preference_persistent:
mPersistent = a.getBoolean(attr, mPersistent);
break;
case com.android.internal.R.styleable.Preference_dependency:
mDependencyKey = a.getString(attr);
break;
case com.android.internal.R.styleable.Preference_defaultValue:
mDefaultValue = onGetDefaultValue(a, attr);
break;
case com.android.internal.R.styleable.Preference_shouldDisableView:
mShouldDisableView = a.getBoolean(attr, mShouldDisableView);
break;
}
}
a.recycle();
if (!getClass().getName().startsWith("android.preference")
&& !getClass().getName().startsWith("com.android")) {
// For non-framework subclasses, assume the worst and don't cache views.
mCanRecycleLayout = false;
}
}
可以看到,这里也没有背景色等设置.但是我们看到了几个关键的属性.mLayoutResId,mWidgetLayoutResId.从名字可以猜的出来他们是一些layout的id.那们是在哪里赋值的呢?至少我们没有手动的给他设置.
按照自定义View的一般套路,拿到属性值就该给拿这些属性值搞事情了,搜啊搜,在Preference中找到了下面这个
private int mLayoutResId = com.android.internal.R.layout.preference;
可以看到mLayoutResId 这个id对应的是系统文件中的com.android.internal.R.layout.preference
该文件在framework/base/core/res/layout目录下.
在线源码网址
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout
xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:minHeight="?android:attr/listPreferredItemHeight"
android:gravity="center_vertical"
android:paddingEnd="?android:attr/scrollbarSize"
android:background="?android:attr/selectableItemBackground" >
<ImageView
android:id="@+android:id/icon"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center"
/>
<RelativeLayout
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="15dip"
android:layout_marginEnd="6dip"
android:layout_marginTop="6dip"
android:layout_marginBottom="6dip"
android:layout_weight="1">
<TextView android:id="@+android:id/title"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:singleLine="true"
android:textAppearance="?android:attr/textAppearanceLarge"
android:ellipsize="marquee"
android:fadingEdge="horizontal" />
<TextView android:id="@+android:id/summary"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_below="@android:id/title"
android:layout_alignStart="@android:id/title"
android:textAppearance="?android:attr/textAppearanceSmall"
android:textColor="?android:attr/textColorSecondary"
android:maxLines="4" />
</RelativeLayout>
<!-- Preference should place its actual preference widget here. -->
<LinearLayout android:id="@+android:id/widget_frame"
android:layout_width="wrap_content"
android:layout_height="match_parent"
android:gravity="center_vertical"
android:orientation="vertical" />
</LinearLayout>
可以看到这个布局文件的样子和我们看到的基本类似.其中有这样一个LinearLayout,看注释就知道这个里边要动态想里边添加控件的.看到这里你应该可以猜到上面提到的mWidgetLayoutResId的作用了吧.
<!-- Preference should place its actual preference widget here. -->
<LinearLayout android:id="@+android:id/widget_frame"
android:layout_width="wrap_content"
android:layout_height="match_parent"
android:gravity="center_vertical"
android:orientation="vertical" />
接下来看SwitchPreference的mWidgetLayoutResId是在哪里赋值的.找这个这个就比较麻烦了.首先你要明白在我们自定义View的时候构造函数中这个几个参数的含义.关键是defStyleAttr的含义,建议参考该地址
public SwitchPreference(Context context, AttributeSet attrs, int defStyleAttr) {
this(context, attrs, defStyleAttr, 0);
}
public SwitchPreference(Context context, AttributeSet attrs) {
this(context, attrs, com.android.internal.R.attr.switchPreferenceStyle);
}
可以看到这里指定了com.android.internal.R.attr.switchPreferenceStyle这样一个属性.我们到系统样式/frameworks/base/core/res/res/values/themes.xml
中找到
<itemname="switchPreferenceStyle">@android:style/Preference.SwitchPreference</item>
下面看看@android:style/Preference.SwitchPreference中的内容
<style name="Preference.SwitchPreference">
<item name="android:widgetLayout">@android:layout/preference_widget_switch</item>
<item name="android:switchTextOn">@android:string/capital_on</item>
<item name="android:switchTextOff">@android:string/capital_off</item>
</style>
这个文件指定了mWidgetLayoutResId所指向的布局文件.
到这里,我们基本上明白了SwitchPreference的UI是如何加载出来的.我们只要把mWidgetLayoutResId,mLayoutResId换乘我们自己的布局文件就可以实现样式的修改.
当然,当我们吧布局文件更换之后,View的id也变了.因此SwitchPreference的onCreateView,onBindView方法需要重写.只需要将系统的id换乘我们自己的id就可以了.
系统源码如下:
@Override
protected void onBindView(View view) {
super.onBindView(view);
View checkableView = view.findViewById(com.android.internal.R.id.switchWidget);
if (checkableView != null && checkableView instanceof Checkable) {
if (checkableView instanceof Switch) {
final Switch switchView = (Switch) checkableView;
switchView.setOnCheckedChangeListener(null);
}
((Checkable) checkableView).setChecked(mChecked);
if (checkableView instanceof Switch) {
final Switch switchView = (Switch) checkableView;
switchView.setTextOn(mSwitchOn);
switchView.setTextOff(mSwitchOff);
switchView.setOnCheckedChangeListener(mListener);
}
}
syncSummaryView(view);
}
@CallSuper
protected View onCreateView(ViewGroup parent) {
final LayoutInflater layoutInflater =
(LayoutInflater) mContext.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
final View layout = layoutInflater.inflate(mLayoutResId, parent, false);
final ViewGroup widgetFrame = (ViewGroup) layout
.findViewById(com.android.internal.R.id.widget_frame);
if (widgetFrame != null) {
if (mWidgetLayoutResId != 0) {
layoutInflater.inflate(mWidgetLayoutResId, widgetFrame);
} else {
widgetFrame.setVisibility(View.GONE);
}
}
return layout;
}
下面给出我的实现,基本上就是照抄SwitchPreference :
public class MySwitchPreference extends SwitchPreference {
private final Listener mListener = new Listener();
private Drawable mIcon;
public MySwitchPreference(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
setLayoutResource(R.layout.preference);//设置自己的更布局
}
public MySwitchPreference(Context context, AttributeSet attrs) {
this(context, attrs, R.attr.mySwitchPreference);//替换成我们自己的样式,同样需要配置自己的主题
}
public MySwitchPreference(Context context) {
this(context, null);
}
@Override
protected View onCreateView(ViewGroup parent) {
super.onCreateView(parent);//自己处理View的创建
final LayoutInflater layoutInflater =
(LayoutInflater) getContext().getSystemService(Context.LAYOUT_INFLATER_SERVICE);
final View layout = layoutInflater.inflate(getLayoutResource(), parent, false);
final ViewGroup widgetFrame = (ViewGroup) layout
.findViewById(R.id.widget_frame);
if (widgetFrame != null) {
if (getWidgetLayoutResource() != 0) {
layoutInflater.inflate(getWidgetLayoutResource(), widgetFrame);
} else {
widgetFrame.setVisibility(View.GONE);
}
}
return layout;
}
@Override
protected void onBindView(View view) {
super.onBindView(view);//自己处理View 的绑定
final TextView titleView = (TextView) view.findViewById(R.id.title);
if (titleView != null) {
final CharSequence title = getTitle();
if (!TextUtils.isEmpty(title)) {
titleView.setText(title);
titleView.setVisibility(View.VISIBLE);
} else {
titleView.setVisibility(View.GONE);
}
}
final ImageView imageView = (ImageView) view.findViewById(R.id.icon);
try {//反射资源id
Class clazz = getClass();
Field Field = clazz.getDeclaredField("mIconResId");
Field.setAccessible(true);
int mIconResId = (int) Field.get(this);
if (imageView != null) {
if (mIconResId != 0 || mIcon != null) {
if (mIcon == null) {
mIcon = getContext().getResources().getDrawable(mIconResId);
}
if (mIcon != null) {
imageView.setImageDrawable(mIcon);
}
}
imageView.setVisibility(mIcon != null ? View.VISIBLE : View.GONE);
}
} catch (NoSuchFieldException e) {
e.printStackTrace();
} catch (IllegalAccessException e) {
e.printStackTrace();
}
View checkableView = view.findViewById(R.id.switchWidget);
if (checkableView != null && checkableView instanceof Checkable) {
if (checkableView instanceof Switch) {
final Switch switchView = (Switch) checkableView;
switchView.setOnCheckedChangeListener(null);
}
((Checkable) checkableView).setChecked(isChecked());
if (checkableView instanceof Switch) {
final Switch switchView = (Switch) checkableView;
switchView.setTextOn(getSwitchTextOn());
switchView.setTextOff(getSwitchTextOff());
switchView.setOnCheckedChangeListener(mListener);
}
}
}
private class Listener implements CompoundButton.OnCheckedChangeListener {
@Override
public void onCheckedChanged(CompoundButton buttonView, boolean isChecked) {
if (!callChangeListener(isChecked)) {
// Listener didn't like it, change it back.
// CompoundButton will make sure we don't recurse.
buttonView.setChecked(!isChecked);
return;
}
MySwitchPreference.this.setChecked(isChecked);
}
}
}