小文字 吃饭,睡觉,遛狗头

教你轻松搞定xml自定义Preference

img

前言

开发中免不了有些设置页面,有设置页面没问题,android 自带Preference相关的api可以很快的实现一套基于xml配置的设置页面;
但是这样就行了么?
现实往往不是这样的,由于UI样式风格布局问题,经常会导致用默认的Preference无法达到需求的效果(这里暂不讨论需求的合理性-_-!)。

方案分析

解决上述问题有很多办法;

  • A.如果只是UI样式的问题,可以考虑基于PreferenceXX的各种控件做微调;
  • B.自定义Preference,也就是写一写符合需求的子控件;
  • C.抛弃Preference,纯手工用view搞定;
  • D.仿照Preference,自己实现一套轻量级的,符合需求的模板;

这四种方案各有长短,可以根据自身情况综合考虑,所谓复用,移植,个性化不可兼得;
ABC三种方案没什么特别,不再赘述,主要看一下方案D,虽然此方案笔者已经在项目中多处稳定使用,但是每个项目需求不同,仅作参考之用.

自己动手,搞定一切

首先确立D方案,需要解决的问题:

  • 我们知道Preference好用是因为他的配置化,根据约定编写xml就可以得到对应的UI视图;我们当然也要这样,用xml简化视图创建;
  • 配置选项对应的基础视图可以自行指定;

主要xml读取,生成合适菜单项,然后配合指定的基础视图元素,批量生成配置页面内的配置项,举个🌰(取自项目案例,仅供参考):

img

XML定义与解析

先来看看XML,我们需要定义三个元素,页面(Screen),配置项(item), 段落分割(divider)有了这三个元素,就可以定义出一个基本页面,如:

<?xml version="1.0" encoding="utf-8"?>
<Screen xmlns:app="http://schemas.android.com/apk/res-auto">

    <item
        app:menu_id="@+id/item_nickname"
        app:menu_label="@string/profile_label_nickname" />
    <item
        app:menu_id="@+id/item_sex"
        app:menu_label="性别" />

    <item
        app:menu_id="@+id/item_introduction"
        app:menu_label="简介" />
    <item
        app:menu_id="@+id/item_company"
        app:menu_label="公司" />

    <divider />

    <item
        app:menu_id="@+id/item_account"
        app:menu_indicator_enable="false"
        app:menu_label="账号" />

    <item
        app:menu_id="@+id/item_register_time"
        app:menu_indicator_enable="false"
        app:menu_label="注册时间" />
</Screen>

定义了xml,需要解析并生产合适的视图

parser = getContext().getResources().getXml(id);
attrs = Xml.asAttributeSet(parser);
int type;
int depth = parser.getDepth();

while (((type = parser.next()) != XmlPullParser.END_TAG || parser.getDepth() > depth) &&
        type != XmlPullParser.END_DOCUMENT) {

    if (type != XmlPullParser.START_TAG) {
        continue;
    }

    String name = parser.getName();
    if (!assertName.equals(name)) {
        throw new IllegalArgumentException("The Screen defined in " +
                getContext().getResources().getResourceEntryName(id) + " must be a <" +
                assertName + " />");
    }

    if (KEY_SCREEN.equals(name)) {
        parseItem(parser, attrs);
    } else {
        throw new IllegalArgumentException("Unknown Screen name " + parser.getName() +
                " in " + getContext().getResources().getResourceEntryName(id));
    }
}

这里用到了XmlPullParser,由于标签定义的很简单,只有Screen,item, divider,首先解析xml是否为一个正确的页面配置文件,也就是要求第一个开始标签是Screen, 满足既可以开始解析后面的item,否则直接抛出错误提示;

解析item的操作是类似的

int type;
int depth = parser.getDepth();
while (((type = parser.next()) != XmlPullParser.END_TAG || parser.getDepth() > depth) &&
        type != XmlPullParser.END_DOCUMENT) {

    if (type != XmlPullParser.START_TAG) {
        continue;
    }
    String name = parser.getName();

    if (KEY_ITEM.equals(name)) {
        TypedArray a = resources.obtainAttributes(attrs, R.styleable.ActionLayout_Item);
        int id = a.getResourceId(R.styleable.ActionLayout_Item_menu_id, -1);
        String title = a.getString(R.styleable.ActionLayout_Item_menu_label);
        String subTitle = a.getString(R.styleable.ActionLayout_Item_menu_sub_label);
        int iconId = a.getResourceId(R.styleable.ActionLayout_Item_menu_icon, -2);
        int iconIndicator = a.getResourceId(R.styleable.ActionLayout_Item_menu_indicator_icon,
                -3);
        String count = a.getString(R.styleable.ActionLayout_Item_menu_count);
        int customLayoutId = -1;
        if (a.hasValue(R.styleable.ActionLayout_Item_menu_custom_layout)) {
            customLayoutId = a.getResourceId(R.styleable.ActionLayout_Item_menu_custom_layout, -1);
        }
        boolean indicatorEnable = a.getBoolean(R.styleable.ActionLayout_Item_menu_indicator_enable, true);
        LogAssist.d(Enum.Developer.CHAOBIN, Enum.Module.LOG, String.format(FORMAT, id, title, iconId, iconIndicator, count));
        a.recycle();
        MenuItem item = new MenuItem();
        item.id = id;
        item.label = title;
        item.subLabel = subTitle;
        item.icon = getDrawable(iconId);
        item.indicatorIcon = iconIndicator > 0 ? getDrawable(iconIndicator) : null;
        item.count = count;
        item.customLayoutId = customLayoutId;
        item.indicatorEnable = indicatorEnable;
        addMenu(item);
    } else if (KEY_DIVIDER.equals(name)) {
        addMenu(true);
    } else {
        LogAssist.d(Enum.Developer.CHAOBIN, Enum.Module.LOG, "Unknown tag found when " +
                "parsing Screen xml: <" + name + "/>");
    }
}

在正确获取到Attribute后,可以开始愉快玩耍,和平时自定义View时读取配置信息一样,通过TypedArray获取我们在xml内配置的数据,并生成相应的model实例;

最后当然是生成View,这个没什么好说的,我们已经读取到了所有的配置信息,可以直接循环遍历批量生成视图, 下面是部分示意代码;

for (Object item : mItems) {
	if (item instanceof MenuItem) {
	    MenuItem menuItem = (MenuItem) item;
	    View view;
	    if (menuItem.customLayoutId != -1) {
	        view = inflater.inflate(menuItem.customLayoutId, null);
	    } else {
	        view = inflater.inflate(mItemLayout, null);
	        //TODO set view
	    }
	    view.setId(menuItem.id);
	    view.setTag(item);
	    view.setOnClickListener(this);
	    addView(view, LayoutParams.MATCH_PARENT, LayoutParams.WRAP_CONTENT);
	    if (item != mItems.get(mItems.size() - 1)) {
	        View line = new View(getContext());
	        line.setBackgroundColor(getResources().getColor(R.color.grey_background));
	        final int height = (int) TypedValue.applyDimension
	                (TypedValue.COMPLEX_UNIT_DIP, 0.5f, getResources().getDisplayMetrics());
	        addView(line, LayoutParams.MATCH_PARENT, Math.max(height, 2));
	    }
	} else if (item instanceof Boolean && (Boolean) item) {
	    View view = inflater.inflate(mDividerLayout, null);
	    addView(view, LayoutParams.MATCH_PARENT, LayoutParams.WRAP_CONTENT);
	}
}

小结

用好配置文件可以做很多又有意思的事,在开发中实现需求的方案很多,多走不寻常的路,发现更大的自由空间;