自定义SwitchPreference样式

先来看看APIDemo中的Preference是什么样子.

image.png

这里个效果实现起来挺简单的,不需要自己写布局文件,只需要在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);
    }
}

}

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 212,029评论 6 492
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,395评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 157,570评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,535评论 1 284
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,650评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,850评论 1 290
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,006评论 3 408
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,747评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,207评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,536评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,683评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,342评论 4 330
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,964评论 3 315
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,772评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,004评论 1 266
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,401评论 2 360
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,566评论 2 349

推荐阅读更多精彩内容