Android自定义View之组合控件

简介

在Android开发中除了View的自定义绘制以外,组合控件也是比较常见的一种自定义View的方式,这种方式通过把系统提供的一些常见View组合起来,添加一些自定义的属性;很少或者根本不需要进行手动的额外绘制,操作相对简单一些,也能大幅度提升开发效率。

在绝大多数app中都有这样的页面:


常见页面

需求整理

从上图我们可以看到这种常见的UI效果中包含了几种情况:

1.选择效果,例如时间的选择,点击右边弹出选择框或者页面跳转,整个item是不可编辑的。
2.输入效果,左边是显示输入框的文本属性,右边是输入框,用户可以输入文本,例如修改个人信息的时候会用到。
3.显示效果,左右都不可编辑,只是单纯的显示一些信息。

除此之外还有一些细节方面的属性,例如:左边和右边文本的属性,颜色,大小,padding值等;item之间的divider分隔线,分隔线的颜色,宽度,padding值;文本框的hint以及hint的细节属性;文字的Gravity属性以及item的点击事件响应等等。

之前为了实现这些效果,大多数人常用的方法就是直接在UI里面进行系统控件的嵌套组合;这是一种最直接也最简单的方法。但是如果类似的效果少了还好,多的话就让人比较头疼了,只能一遍一遍复制粘贴。这样就会造成页面布局文件很长很长;系统解析起来费劲,自己看着也烦。

这时候自定义组合控件就派上用场了。

实现原理

先通过分析,把类似的效果抽取到一个通用的UI中。

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout
    xmlns:android="http://schemas.android.com/apk/res/android"
    android:id="@+id/ll_view_container"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:background="@android:color/white"
    android:gravity="center_vertical"
    android:orientation="horizontal">

    <TextView
        android:id="@+id/tv_choose_item_name"
        android:layout_width="wrap_content"
        android:layout_height="match_parent"
        android:padding="15dp"
        android:textColor="@android:color/black"/>

    <TextView
        android:id="@+id/tv_choose_item_choose"
        android:layout_width="0dp"
        android:layout_height="match_parent"
        android:layout_weight="100"
        android:drawablePadding="15dp"
        android:maxLines="1"
        android:ellipsize="end"
        android:padding="15dp"
        android:textColor="@android:color/black"/>

    <EditText
        android:id="@+id/et_choose_item_choose"
        android:layout_width="0dp"
        android:layout_height="match_parent"
        android:layout_weight="100"
        android:background="@null"
        android:paddingLeft="15dp"
        android:paddingBottom="15dp"
        android:paddingTop="15dp"
        android:paddingRight="50dp"
        android:maxLines="1"
        android:ellipsize="end"
        android:textColor="@android:color/black"
        android:visibility="gone"/>
</LinearLayout>

总的来说就是一个TextView+一个EditText的组合,或者两个TextView的组合;或者可以把右箭头图标单独拎出来放进一个ImageView中。

布局抽取出来之后就开始进行控件组合代码的编写:

先继承LinearLayout并添加构造方法,添加自定义的属性:

<?xml version="1.0" encoding="utf-8"?>
<resources>
    <declare-styleable name="ChooseItemView">
        <attr name="item_name_text" format="string|reference"/><!--item名称-->
        <attr name="item_name_text_padding_left" format="dimension|reference"/>
        <attr name="item_height" format="dimension|reference"/>
        <attr name="item_name_text_color" format="color|reference"/>
        <attr name="item_name_text_size" format="dimension|reference"/>
        <attr name="item_choose_text" format="string|reference"/>
        <attr name="item_choose_text_color" format="color|reference"/>
        <attr name="item_choose_text_size" format="dimension|reference"/>
        <attr name="item_choose_icon_visible" format="boolean"/>
        <attr name="item_edit_mode" format="boolean"/>
        <attr name="item_edit_hint" format="string|reference"/>
        <attr name="item_edit_text" format="string|reference"/>
        <attr name="item_edit_text_size" format="dimension|reference"/>
        <attr name="item_edit_text_color" format="color|reference"/>
        <attr name="item_edit_hint_color" format="color|reference"/>
        <attr name="item_divider_color" format="color|reference"/>
        <attr name="item_divider_height" format="reference|dimension"/>
        <attr name="item_divider_margin_left" format="reference|dimension"/>
        <attr name="item_divider_margin_right" format="reference|dimension"/>
        <attr name="item_choose_gravity" format="enum">
            <enum name="left" value="0"/>
            <enum name="center" value="1"/>
            <enum name="right" value="2"/>
        </attr>
        <attr name="item_edit_text_gravity" format="enum">
            <enum name="left" value="0"/>
            <enum name="center" value="1"/>
            <enum name="right" value="2"/>
        </attr>
    </declare-styleable>
</resources>
public class ChooseItemView extends LinearLayout
{
  
    public ChooseItemView(Context context)
    {
        this(context, null, 0);
    }

    public ChooseItemView(Context context, AttributeSet attrs)
    {
        this(context, attrs, 0);
    }

    public ChooseItemView(Context context, AttributeSet attrs, int defStyleAttr)
    {
        super(context, attrs, defStyleAttr);
        this.context = context;
        //抽取需要的自定义属性
        TypedArray typedArray = context.obtainStyledAttributes(attrs, R.styleable.ChooseItemView);
        itemNameText = typedArray.getString(R.styleable.ChooseItemView_item_name_text);
        itemNameTextPaddingLeft = (int) typedArray.getDimension(R.styleable.ChooseItemView_item_name_text_padding_left, dip2px(15));
        itemNameTextColor = typedArray.getColor(R.styleable.ChooseItemView_item_name_text_color, Color.BLACK);
        itemNameTextSize = (int) typedArray.getDimension(R.styleable.ChooseItemView_item_name_text_size, 16);
        itemChooseText = typedArray.getString(R.styleable.ChooseItemView_item_choose_text);
        itemChooseTextColor = typedArray.getColor(R.styleable.ChooseItemView_item_choose_text_color, Color.BLACK);
        itemChooseTextSize = (int) typedArray.getDimension(R.styleable.ChooseItemView_item_choose_text_size, 16);
        itemChooseGravity = typedArray.getInt(R.styleable.ChooseItemView_item_choose_gravity, 2);
        editTextGravity = typedArray.getInt(R.styleable.ChooseItemView_item_edit_text_gravity, 2);
        iconVisible = typedArray.getBoolean(R.styleable.ChooseItemView_item_choose_icon_visible, true);

        editMode = typedArray.getBoolean(R.styleable.ChooseItemView_item_edit_mode, false);
        editHint = typedArray.getString(R.styleable.ChooseItemView_item_edit_hint);
        editHintColor = typedArray.getColor(R.styleable.ChooseItemView_item_edit_hint_color, Color.LTGRAY);
        editText = typedArray.getString(R.styleable.ChooseItemView_item_edit_text);
        editTextColor = typedArray.getColor(R.styleable.ChooseItemView_item_edit_text_color, Color.BLACK);
        editTextSize = (int) typedArray.getDimension(R.styleable.ChooseItemView_item_edit_text_size, 16);

        dividerColor = typedArray.getColor(R.styleable.ChooseItemView_item_divider_color, Color.LTGRAY);
        dividerHeight = (int) typedArray.getDimension(R.styleable.ChooseItemView_item_divider_height, dip2px(2));
        dividerMarinLeft = (int) typedArray.getDimension(R.styleable.ChooseItemView_item_divider_margin_left, dip2px(15));
        dividerMarinRight = (int) typedArray.getDimension(R.styleable.ChooseItemView_item_divider_margin_right, 0);
        itemHeight = (int) typedArray.getDimension(R.styleable.ChooseItemView_item_height, dip2px(50))+dividerHeight/2;

        typedArray.recycle();
  
    }

进行子View的绑定和属性初始化:

    private void initView()
    {
        //准备绘制分隔线的画笔
        paint = new Paint(Paint.ANTI_ALIAS_FLAG);
        paint.setStyle(Paint.Style.FILL);
        paint.setColor(dividerColor);
        paint.setStrokeWidth(dividerHeight);
        
        //子控件绑定
        View view = LayoutInflater.from(context).inflate(R.layout.layout_choose_item, this);

        llContainer = (LinearLayout) view.findViewById(R.id.ll_view_container);
        tvItemName = (TextView) view.findViewById(R.id.tv_choose_item_name);
        tvItemChoose = (TextView) view.findViewById(R.id.tv_choose_item_choose);
        etItemChoose = (EditText) view.findViewById(R.id.et_choose_item_choose);

        //初始化各个控件的属性
        tvItemName.setText(itemNameText);
        tvItemName.setTextColor(itemNameTextColor);
        tvItemName.setTextSize(itemNameTextSize);
        tvItemName.setPadding(itemNameTextPaddingLeft, dip2px(15), dip2px(15), dip2px(15));


        tvItemChoose.setText(itemChooseText);
        tvItemChoose.setTextColor(itemChooseTextColor);
        tvItemChoose.setTextSize(itemChooseTextSize);

        //item整体高度的自定义
        ViewGroup.LayoutParams params = llContainer.getLayoutParams();
        params.height = itemHeight;
        llContainer.setLayoutParams(params);

        etItemChoose.setText(editText);
        etItemChoose.setHint(editHint);
        etItemChoose.setHintTextColor(editHintColor);
        etItemChoose.setTextSize(editTextSize);
        etItemChoose.setTextColor(editTextColor);

        setEditMode(editMode);
        setIconVisible(iconVisible);
        setItemChooseGravity(itemChooseGravity);
        setEditTextGravity(editTextGravity);

    }

绘制item之间的分隔线:

    @Override
    protected void dispatchDraw(Canvas canvas)
    {
        //绘制分隔线 一定要在父控件绘制完成之后
        super.dispatchDraw(canvas);
        canvas.drawLine(dividerMarinLeft, itemHeight, getWidth() - dividerMarinRight, itemHeight, paint);
    }

添加一些常用的api以便在代码中修改相关属性:

    //自定义属性相关的api
    public String getItemNameText()
    {
        return itemNameText;
    }

    public void setItemNameText(String itemNameText)
    {
        this.itemNameText = itemNameText;
        tvItemName.setText(itemNameText);
    }
    public void setItemNameTextSize(int itemNameTextSize)
    {
        this.itemNameTextSize = itemNameTextSize;
        tvItemName.setTextSize(TypedValue.COMPLEX_UNIT_SP, itemNameTextSize);
    }

    public void setItemChooseTextColor(int itemChooseTextColor)
    {
        this.itemChooseTextColor = itemChooseTextColor;
        tvItemChoose.setTextColor(ContextCompat.getColor(context, itemChooseTextColor));
    }

    public int getItemChooseTextSize()
    {
        return itemChooseTextSize;
    }

    public void setItemChooseTextSize(int itemChooseTextSize)
    {
        this.itemChooseTextSize = itemChooseTextSize;
        tvItemChoose.setTextSize(TypedValue.COMPLEX_UNIT_SP, itemChooseTextSize);
    }
    //与绘制相关的属性修改完成后要记得通知系统进行重绘
    public void setDividerColor(int dividerColor)
    {
        this.dividerColor = dividerColor;
        paint.setColor(ContextCompat.getColor(context, dividerColor));
        invalidate();
    }
    //根据需要添加对应的监听事件
    public void setIconClickListener(OnClickListener iconClickListener)
    {
        tvItemChoose.setOnClickListener(iconClickListener);
    }

    public void setTextChangeListener(TextWatcher textChangeListener)
    {
        etItemChoose.addTextChangedListener(textChangeListener);
    }
}
....

效果和总结

使用:

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout
    xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:background="#fff"
    android:orientation="vertical">

    <com.mewlxy.itemchooseview.view.ChooseItemView
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        app:item_choose_text="2017-08-27"
        app:item_name_text="选择时间"/>

    <com.mewlxy.itemchooseview.view.ChooseItemView
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        app:item_edit_hint="请输入姓名"
        app:item_edit_mode="true"
        app:item_name_text="输入姓名"/>

    <com.mewlxy.itemchooseview.view.ChooseItemView
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        app:item_divider_color="@android:color/holo_purple"
        app:item_divider_height="10dp"
        app:item_divider_margin_left="0dp"
        app:item_edit_hint="请输入姓名"
        app:item_edit_hint_color="#66ccbb"
        app:item_edit_mode="true"
        app:item_name_text="输入姓名"
        app:item_name_text_color="#66ccff"/>

    <com.mewlxy.itemchooseview.view.ChooseItemView
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        app:item_choose_gravity="left"
        app:item_choose_icon_visible="false"
        app:item_choose_text="姓名"
        app:item_choose_text_color="@color/colorAccent"
        app:item_divider_margin_left="0dp"
        app:item_edit_hint_color="#66ccbb"
        app:item_name_text="输入姓名"
        app:item_name_text_color="#66ccff"/>

</LinearLayout>
效果.png

这样完了之后,一个通用的UI控件就完成了,可以适用于大多数类似的页面,仅仅需要设置几个不同的属性;成倍的减少了代码量,布局文件再也不会像以前那样臃肿了。并且代码的扩展性也增强了不少。以后遇到类似的场景就可以参考这种实现方式,而不必一次次的复制粘贴。

完整代码参考

GitHub

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

推荐阅读更多精彩内容

  • Android 自定义View的各种姿势1 Activity的显示之ViewRootImpl详解 Activity...
    passiontim阅读 172,245评论 25 707
  • 关在黑屋子里太久,渐渐开始不习惯阳光的味道,那刺眼的温暖也成了曝光的负累。有时候会纠结,自己到底是属于哪一个领域的...
    夜白安阅读 263评论 0 0
  • 今天按照书上说的Q版漫画画了几个人脸~ —— ——! Fathing
    鲁狂歌阅读 123评论 0 0
  • 第一、问过多问题 或者是搭讪焦虑、缺少话题、等原因,男生喜欢在和女生互动的时候问过多的问题,并且无意识地把话题的主...
    繁越S阅读 313评论 0 0
  • 夏天除了温度高,最烦人的就是下雨天,而且一下就是好几天,最可怜的是上班族,一场暴雨过后,再美的穿搭也变得毫无亮点。...
    蜘蜘纺阅读 353评论 0 0