Android 自定义复合控件之通用标题栏

效果图

估计大家应该和我一样,每次去看别人博客的时候,都喜欢一拉到底,先看看有没有效果图,符不符合自己的需求,符合咱就继续看,不符合免得浪费表情,所以效果图先上为敬

自定义标题栏.png

写在前面的一点儿废话

作为Android的菜鸟一枚,一直觉得能够写自定义控件是一个很炫酷的技能,最近看了徐宜生老师的群英传之后,感觉收获还是挺多的。这篇文章就主要记录的是学习自定义控件中最简单的复合控件的过程。虽然现在MD中Toolbar已经完全满足各种各样的需求,但对于我这种菜鸟来说自己动手写一个还是能学到很多东西的!

1、自定义控件的属性

既然是自定义的控件,肯定得提供属性选项,以方便实现不同的样式。提供自定义的属性是很简单的,在res资源目录下的values目录下创建一个attrs.xml的属性集定义的xml文件,在该文件中自定义各种必要的属性

<?xml version="1.0" encoding="utf-8"?>
<resources>

    <declare-styleable name="NormalTopBar">
        <!--中间标题属性-->
        <attr name="titleText" format="string"/>
        <attr name="titleTextSize" format="dimension"/>
        <attr name="titleTextColor" format="color"/>
        <attr name="leftText" format="string"/>

        <!--左边按钮属性-->
        <attr name="leftTextSize" format="dimension"/>
        <attr name="leftTextColor" format="color"/>
        <attr name="leftImageSrc" format="reference"/>
        <attr name="rightText" format="string"/>

        <!--右边按钮属性-->
        <attr name="rightTextSize" format="dimension"/>
        <attr name="rightTextColor" format="color"/>
        <attr name="leftBackground" format="color"/>
        <attr name="rightBackground" format="color"/>
        <attr name="rightImageSrc" format="reference"/>
    </declare-styleable>
</resources>

既然自定义了属性,就需要在自定义控件模板中去获取这些属性的赋值,以处理得到相应的显示效果。在这里,系统提供了TypeArray类,获取到该类的实例后就可通过getString()等方法获得布局文件中设置的属性值

 private void getTypeArray(Context context, AttributeSet attrs) {
        //将attrs.xml中定义的属性存储到TypeArray中
        TypedArray typeArray=context.obtainStyledAttributes(attrs,R.styleable.NormalTopBar);

        leftText=typeArray.getString(R.styleable.NormalTopBar_leftText);
        leftTextColor=typeArray.getColor(R.styleable.NormalTopBar_leftTextColor, Color.BLACK);
        leftTextSize=typeArray.getDimension(R.styleable.NormalTopBar_leftTextSize,12);
        leftImageId=typeArray.getResourceId(R.styleable.NormalTopBar_leftImageSrc,0);
        titleText=typeArray.getString(R.styleable.NormalTopBar_titleText);
        titleTextColor=typeArray.getColor(R.styleable.NormalTopBar_titleTextColor,Color.BLACK);
        titleTextSize=typeArray.getDimension(R.styleable.NormalTopBar_titleTextSize,20);
        rightText=typeArray.getString(R.styleable.NormalTopBar_rightText);
        rightTextColor=typeArray.getColor(R.styleable.NormalTopBar_rightTextColor,Color.BLACK);
        rightTextSize=typeArray.getDimension(R.styleable.NormalTopBar_rightTextSize,12);
        rightImageId=typeArray.getResourceId(R.styleable.NormalTopBar_rightImageSrc,0);

        typeArray.recycle();//获取完所有属性后需要调用recycle来避免重新创建发生的错误

    }

参数中attrs是控件构造函数中传入的属性集参数,而R.styleable.NormalTopBar就是在attrs.xml文件中定义的该控件属性集的名字。

2、动态添加控件组合成自定义符合控件

标题栏中一般包括了左边的按钮,中间的标题,右边的按钮。在本文中,我把该控件分成了5个部分,左边有一个ImageView和一个TextView用于用户点击,中间有一个TextView用于显示标题,右边和左边一样,成对称分布,然后这些控件的父控件是RelativeLayout,方便子控件的布局。了解了有哪些控件之后,就可以初始化这些控件对象,然后分别指定合适的布局,动态添加布局中。

 private void addAllView(Context context) {
        leftTextView =new TextView(context);
        rightTextView =new TextView(context);
        titleTextView=new TextView(context);
        leftImage=new ImageView(context);
        rightImage=new ImageView(context);

        leftImage.setId(R.id.leftimageid);
        leftImage.setImageResource(leftImageId);
        //leftImage.setAdjustViewBounds(true);

        leftTextView.setText(leftText);
        leftTextView.setTextSize(leftTextSize);
        leftTextView.setTextColor(leftTextColor);

        titleTextView.setText(titleText);
        titleTextView.setTextSize(titleTextSize);
        titleTextView.setTextColor(titleTextColor);
        titleTextView.setGravity(Gravity.CENTER);//一定要设置textview内容的位置

        rightTextView.setText(rightText);
        rightTextView.setTextSize(rightTextSize);
        rightTextView.setTextColor(rightTextColor);

        rightImage.setId(R.id.rightimageid);
        rightImage.setImageResource(rightImageId);




        //为组建设置相应的布局

        if(leftImageId!=0&&leftText!=null){
            leftImageParams=new LayoutParams(dpToPx(context,35), dpToPx(context,35));
            leftImageParams.addRule(ALIGN_PARENT_LEFT,TRUE);
            leftImageParams.addRule(CENTER_VERTICAL,TRUE);
            addView(leftImage,leftImageParams);

            leftTextParams =new LayoutParams(ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT);
            leftTextParams.addRule(RelativeLayout.RIGHT_OF,R.id.leftimageid);
            leftTextParams.addRule(CENTER_VERTICAL,TRUE);
            leftTextView.setGravity(Gravity.LEFT);
            addView(leftTextView, leftTextParams);
        }else if(leftImageId!=0&&leftText==null){
            leftImageParams=new LayoutParams(dpToPx(context,35), dpToPx(context,35));
            leftImageParams.addRule(ALIGN_PARENT_LEFT,TRUE);
            leftImageParams.addRule(CENTER_VERTICAL,TRUE);
            addView(leftImage,leftImageParams);
        }else{
            leftTextParams =new LayoutParams(ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT);
            leftTextParams.addRule(RelativeLayout.ALIGN_PARENT_LEFT,TRUE);
            leftTextParams.addRule(CENTER_VERTICAL,TRUE);
            addView(leftTextView, leftTextParams);
        }


        titleParams=new LayoutParams(ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.MATCH_PARENT);
        titleParams.addRule(CENTER_IN_PARENT,TRUE);
        titleParams.addRule(TEXT_ALIGNMENT_CENTER);
        addView(titleTextView,titleParams);

        if(rightImageId!=0&&rightText!=null){
            rightTextParams =new LayoutParams(ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT);
            rightTextParams.addRule(RelativeLayout.LEFT_OF,R.id.rightimageid);
            rightTextParams.addRule(RelativeLayout.CENTER_VERTICAL,TRUE);
            rightTextView.setGravity(Gravity.RIGHT);
            addView(rightTextView, rightTextParams);
            rightImageParams=new LayoutParams(dpToPx(context,35), dpToPx(context,35));
            rightImageParams.addRule(CENTER_VERTICAL,TRUE);
            rightImageParams.addRule(ALIGN_PARENT_RIGHT,TRUE);
            addView(rightImage,rightImageParams);
        }else if(rightImageId!=0&&rightText==null){
            rightImageParams=new LayoutParams(dpToPx(context,35), dpToPx(context,35));
            rightImageParams.addRule(CENTER_VERTICAL,TRUE);
            rightImageParams.addRule(ALIGN_PARENT_RIGHT,TRUE);
            addView(rightImage,rightImageParams);
        }else{
            rightTextParams =new LayoutParams(ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT);
            rightTextParams.addRule(RelativeLayout.ALIGN_PARENT_RIGHT,TRUE);
            rightTextParams.addRule(RelativeLayout.CENTER_VERTICAL,TRUE);
            addView(rightTextView, rightTextParams);
        }

    }

这一段代码首先是初始化得到各子控件的实例对象,然后将属性值赋值给对应的控件,接着利用LayoutParams类对各子空间的大小和位置进行设定,最后利用addView方法即可将这些子控件添加到控件整体布局中。

这段代码中,主要的难点在于运用LayoutParams,要注意该布局的外层viewGroup是RelativeLayout,所以在定义和初始化的时候都需要使用RelativeLayout.LayoutParams.另外LayoutParams的构造函数中的参数用于控制大小,我在设置ImageView对应的LayoutParams时,最开始把宽和高都设置为WRAP_CONTENT,但是运行后效果不理想,imageview宽度占据了一半的空间,最后决定对该控件的大小指定尺寸大小,不过要注意构造函数中的数值单位是px,所以需要先定义一个函数将dp转为px再赋值给构造函数。

这段代码的另外一个难点是,当我两侧的按钮同时有文字和图标时,对于ImageView和TextView的定位是个问题。在下面代码中

leftTextParams.addRule(RelativeLayout.ALIGN_PARENT_LEFT,TRUE);

这行代码将TextView定位在了父控件的左侧,而当左侧同时有ImageView和TextView时二者肯定就会重叠,这肯定不是想要的结果。所以需要把ImageView仍定位在最左边,然后TextView定位在前者的右边,而在方法addRule()中,可以使用 addRule(RelativeLayout.RIGHT_OF,int view) 来把对应的控件定位在参数中view控件的右边,但是该参数需要的是资源ID,可问题是在上面我们是动态添加的ImageView,并没有在xml文件中定义id。我尝试了直接用imageview.getId(),但得到的结果经调试发现是-1,并不能实现想要的效果,最后一搜找到了一个方法,首先在资源目录res下的values下再新建一个ids.xml的文件,然后在文件中定义一个类型为id的item

<?xml version="1.0" encoding="utf-8"?>
<resources>
    <item name="leftimageid" type="id"/>
    <item name="rightimageid" type="id"/>
</resources>

然后利用ImageView.setId(R.id.leftimageid) 就能给动态添加的控件赋值一个不会与其他资源id重复的id,接着就可以在布局中使用。

3、定义接口暴露给调用者

到目前位置,编写的自定义控件已经可以在xml布局文件中使用,而且也能在界面上显示出来,但是左右两侧的按钮点击事件对于不同的使用者或者不同的页面,所要完成的动作肯定是不一样的,所以得暴露一个接口给调用者自己去实现。

public interface normalTopClickListener{
        void onLeftClick(View view);
        void onRightClick(View view);
    }

然后给调用者提供一个set函数让调用者来实现该接口中的方法

 public void setTopClickListener(normalTopClickListener mListener){
        this.mClickListener =mListener;
    }

最后在控件模板中,在左右控件的点击事件里去调用接口的方法,即可得到调用者的具体实现

private void addOnClick() {

        leftTextView.setOnClickListener(new OnClickListener() {
            @Override
            public void onClick(View view) {
                mClickListener.onLeftClick(view);
            }
        });

        leftImage.setOnClickListener(new OnClickListener() {
            @Override
            public void onClick(View view) {
                mClickListener.onLeftClick(view);
            }
        });

        rightTextView.setOnClickListener(new OnClickListener() {
            @Override
            public void onClick(View view) {
                mClickListener.onRightClick(view);
            }
        });

        rightImage.setOnClickListener(new OnClickListener() {
            @Override
            public void onClick(View view) {
                mClickListener.onRightClick(view);
            }
        });

    }

github源码

结语

终于写完了第一篇博客,说句实在的,第一次写起来感觉真不简单。如果文中有任何错误或者建议,欢迎指出,不胜感激

2017.3.20 23:45
806实验室

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

推荐阅读更多精彩内容