自定义View实现设置中心的功能视图

我们经常会遇到这么一个问题,同一个界面中有几个相同的布局,如果这些布局是一些简单的控件,重复写几遍也无所谓,但是如果这些布局里边嵌套了很多控件,甚至布局里边嵌套布局,重复写工作量很大,这时我们需要用自定义View来减少工作量,这里利用自定义View实现设置中心的功能视图。

问题的引出


我有这么一个需求,在设置中心可以设置对电话短信的拦截是否开启,如下图:

实现上面视图,需要的布局文件如下:

<?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="match_parent"
    android:orientation="vertical">

    <TextView
        style="@style/text_title_style"
        android:text="设置中心" />

    <RelativeLayout
        android:id="@+id/rl_sms_block_setting"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:layout_marginLeft="5dp"
        android:layout_marginTop="5dp">

        <TextView
            android:id="@+id/tv_sms_block_setting"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:text="电话短信拦截设置"
            android:textColor="#000000"
            android:textSize="23sp" />

        <TextView
            android:id="@+id/tv_sms_block_state"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_below="@+id/tv_sms_block_setting"
            android:layout_marginTop="3dp"
            android:text="黑名单拦截没有开启"
            android:textSize="18sp" />

        <CheckBox
            android:id="@+id/cb_sms_block"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_alignParentRight="true"
            android:layout_centerVertical="true"
            android:clickable="false"
            android:focusable="false" />
    </RelativeLayout>

</LinearLayout>

实现电话短信拦截是否开启就需要以上几十行代码,如果在下面还需实现版本更新是否开启,我们可能会复制以上几十行代码,修改其中的文本即可,但是如果还有同样的10个功能呢,我们也继续复制吗?显然没必要,使用自定义View,相同的布局只需写一次。

自定义View实现设置中心的功能视图Demo


把上边布局文件中的需要重复书写的布局提取出来,放到一个单独的布局文件中,以供自定义View的引用。

item布局文件:ui_item_setting.xml

<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:id="@+id/rl_sms_block_setting"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:layout_marginLeft="5dp"
    android:layout_marginTop="5dp">

    <TextView
        android:id="@+id/tv_title"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:textColor="#000000"
        android:textSize="23sp" />

    <TextView
        android:id="@+id/tv_desc"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_below="@+id/tv_title"
        android:layout_marginTop="3dp"
        android:textSize="18sp" />

    <CheckBox
        android:id="@+id/cb_checked"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_alignParentRight="true"
        android:layout_centerVertical="true"
        android:clickable="false"
        android:focusable="false" />
</RelativeLayout>

自定义View的Java代码:SettingView.java

public class SettingsView extends RelativeLayout {

    private TextView tvTitle;
    private TextView tvDesc;
    private CheckBox cbCheaked;

    public SettingsView(Context context) {
        super(context);
        initView(context);
    }

    //如果使用布局文件创建view对象,会调用第二个构造方法
    public SettingsView(Context context, AttributeSet attrs) {
        super(context, attrs);
        initView(context);
    }

    public SettingsView(Context context, AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
        initView(context);
    }

    private void initView(Context context) {
        View.inflate(context, R.layout.ui_item_setting, this);
        tvTitle = (TextView) this.findViewById(R.id.tv_title);
        tvDesc = (TextView) this.findViewById(R.id.tv_desc);
        cbCheaked = (CheckBox) this.findViewById(R.id.cb_checked);
    }

    /**
     * 设置文本标题
     * @param text
     */
    public void setTitle(String text) {
        tvTitle.setText(text);
    }

    /**
     * 设置文本描述
     * @param text
     */
    public void setDesc(String text) {
        tvDesc.setText(text);
    }

    /**
     * 设置checkbox
     * @param checked
     */
    public void setChecked(boolean checked) {
        cbCheaked.setChecked(checked);
    }

    /**
     * 获取文本标题
     */
    public String getTitle() {
        return tvTitle.getText().toString();
    }

    /**
     * 获取文本描述
     */
    public String getDesc() {
        return tvDesc.getText().toString();
    }

    /**
     * 获取checkbox状态
     */
    public boolean getChecked() {
        return cbCheaked.isChecked();
    }
}

从最上边的布局文件中我们可以看出,需要重复书写的布局是一个RelativeLayout,所以新建一个类SettingView继承RelativeLayout,并实现SettingView的三个构造方法,为了保证一创建或引用SettingView就能将布局转化为View对象,在三个构造方法中都实现了一个初始化View对象的initView方法。

在initView方法中,利用View.inflate(Context context, int resource, ViewGroup root)方法将布局转化为View对象,这个方法有三个参数,前两个参数分别为上下文和抽取出来新建的布局文件,第三个参数是一个ViewGroup(View的容器),如果创建一个单独的View对象,用null即可,如果把一个布局转化为View对象,并挂载在自定义View中,则用this。所以View.inflate(context, R.layout.ui_item_setting, this);的意思是将ui_item_setting显示到SettingView中。

我们还可以看到SettingView中还定义了一些getter和setter方法,这些方法是为了操作(设置和获取)文本而创建的。

自定义View基本搞定,接下来就可以在布局文件中使用自定义的SettingView了。

页面布局文件:activity_setting.xml

<?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="match_parent"
    android:orientation="vertical">

    <TextView
        style="@style/text_title_style"
        android:text="设置中心" />

    <com.trampcr.mobilesafer.ui.SettingsView
        android:id="@+id/sv_sms_block"
        android:layout_width="match_parent"
        android:layout_height="wrap_content">
    </com.trampcr.mobilesafer.ui.SettingsView>

</LinearLayout>

把SettingsView的全路径作为一个标签写到布局文件当中,就已经完了自定义View的引用。

接下来就可以创建SettingsView 对象了,并操作该SettingsView 对象,例如为其添加点击事件等等。

主活动代码:SettingActivity.java

public class SettingsActivity extends Activity {

    private SettingsView svSmsBlock;
    private SharedPreferences sp;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_settings);
        svSmsBlock = (SettingsView) findViewById(R.id.sv_sms_block);
        sp = getSharedPreferences("config", MODE_PRIVATE);

        if (sp.getBoolean("smsblock", false)){
            svSmsBlock.setChecked(true);
            svSmsBlock.setDesc("黑名单拦截已经开启");
        }else {
            svSmsBlock.setChecked(false);
            svSmsBlock.setDesc("黑名单拦截没有开启");
        }

        svSmsBlock.setTitle("电话短信拦截设置");
        svSmsBlock.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                SharedPreferences.Editor editor = sp.edit();
                if (svSmsBlock.getChecked()) {
                    svSmsBlock.setChecked(false);
                    svSmsBlock.setDesc("黑名单拦截没有开启");
                    editor.putBoolean("smsblock", false);
                } else {
                    svSmsBlock.setChecked(true);
                    svSmsBlock.setDesc("黑名单拦截已经开启");
                    editor.putBoolean("smsblock", true);
                }
                editor.commit();
            }
        });
    }
}

上面这部分主要实现了SettingsView 对象的点击事件,将Checkbox的状态保存在SharedPreferences中,通过判断SharedPreferences保存的状态信息来显示相应的文本。

到这里还有一个缺陷,那就是设置文本,需要先获取自定义控件对象,然后通过setTitle和setDesc来设定,加一个控件就需要加一段代码,显然有些繁琐,那么如何实现像TextView那样,直接在属性里就可以定义文本呢?

首先先了解一下我们平时用的属性是哪里来的,观察xml文件发现每个头布局中都有这么一行代码:

xmlns:android="http://schemas.android.com/apk/res/android"

xmlns意思是xml命名空间,android就是命名空间的名称,所以每个属性都是android:开头的,而后面的一串字符是依赖的android.jar的路径。

AndroidStudio中不用自定义命名空间,在下边创建好attrs.xml后即可自动生成一个命名空间:

xmlns:app="http://schemas.android.com/apk/res-auto"

要使用属性,需要先声明属性。通过查资料得知android系统的这些属性放在sdk/platform/android-version/data/res/values/attrs.xml中,这里展示一下TextView控件的部分属性定义:

<declare-styleable name="TextView">
        <attr name="bufferType">
            <enum name="normal" value="0" />
            <enum name="spannable" value="1" />
            <enum name="editable" value="2" />
        </attr>
        <attr name="text" format="string" localization="suggested" />
        <attr name="hint" format="string" />
...

模仿系统控件定义属性的方法,在values下新建一个attrs.xml:

<?xml version="1.0" encoding="utf-8"?>
<resources>
    <declare-styleable name="SettingsView">
        <attr name="title_text" format="string"/>
        <attr name="desc_on" format="string"/>
        <attr name="desc_off" format="string"/>
    </declare-styleable>
</resources>

接下来就可以使用自定义控件的属性了。

<com.trampcr.mobilesafer.ui.SettingsView
        android:id="@+id/sv_sms_block"
        app:title_text="电话短信拦截设置"
        app:desc_on="黑名单拦截已经开启"
        app:desc_off="黑名单拦截没有开启"
        android:layout_width="match_parent"
        android:layout_height="wrap_content">
</com.trampcr.mobilesafer.ui.SettingsView>

自定义控件的属性可以使用了,接下来就是如何将属性设定的内容显示到界面上,回到自定义控件的三个构造方法,前面提到过如果使用布局文件创建View对象,会调用那个含有两个参数的构造方法,这个构造方法的第二个参数是一个AttributeSet,要想获取属性中设定的文本可以通过AttributeSet的getAttributeValue方法,该方法的第一个参数是命名空间,第二个参数是属性名称,那么获取属性中设定的文本的代码如下:

...
//如果使用布局文件创建view对象,会调用第二个构造方法
public SettingsView(Context context, AttributeSet attrs) {
        super(context, attrs);
        initView(context);
        title = attrs.getAttributeValue("http://schemas.android.com/apk/res-auto", "title_text");
        descOn = attrs.getAttributeValue("http://schemas.android.com/apk/res-auto", "desc_on");
        descOff = attrs.getAttributeValue("http://schemas.android.com/apk/res-auto", "desc_off");
        setTitle(title);
        setDesc(descOff);
}
...
public void setChecked(boolean checked) {
        if (checked){
            setDesc(descOn);
        }else {
            setDesc(descOff);
        }
}

在构造方法中获取属性中设定的文本,并设置默认的标题和描述信息,这样在代码中就不用再显示设置文本了;并通过判断勾选状态来设置描述信息。

到目前为止,到目前为止,到目前为止,通过自定义View实现了文章刚开始的那个界面,你可能会说自定义View也就如此,但是接下来你就会体验到它的强大和方便了,比如还需要一个更新状态是否开启的设置,只需在布局文件中添加一个SettingView标签,然后在主界面添加该控件的点击事件即可。xml文件和Java代码如下:

<com.trampcr.mobilesafer.ui.SettingsView
        android:id="@+id/sv_update"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        app:desc_off="更新设置没有开启"
        app:desc_on="更新设置已经开启"
        app:title_text="更新状态设置"/>
        svUpdate.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                SharedPreferences.Editor editor = sp.edit();
                if (svUpdate.getChecked()) {
                    svUpdate.setChecked(false);
                    editor.putBoolean("update",false);
                }else {
                    svUpdate.setChecked(true);
                    editor.putBoolean("update",true);
                }
                editor.commit();
            }
        });

添加后的界面如下:

如果再添加其他的设置,只需添加以上两段代码即可,简单方便。

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

推荐阅读更多精彩内容